android 從源碼分析view事件分發機制

一直對View的事件分發機制不太明白,在項目開發中也遇到過,在網上也找到一些解決問題方法,但是其原理並不太瞭解,現在辭職瞭有時間,今天寫寫View的事件分發,結合android源碼一起來學習下,如果講的不對,往指出一起學習提高,言歸正傳。

新建一個android項目,裡面隻有一個activity,有一個button,我們給Button設置setOnClickListener(),setOnTouchListener(),通過log看看結果:

 

btnClick.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				Log.i(com.example.demo,button click );
			}
		});

Button的Touch事件:

 

 

btnClick.setOnTouchListener(new OnTouchListener() {
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				Log.i(com.example.demo,button touch +event.getAction());
				return false;
			}
		});

log:

 

 

com.example.demo(30220): button touch 0
com.example.demo(30220): button touch 2
com.example.demo(30220): button touch 2
com.example.demo(30220): button touch 2
com.example.demo(30220): button touch 2
com.example.demo(30220): button touch 2
com.example.demo(30220): button touch 1
com.example.demo(30220): button click 

可以看到onTouch優先於OnClick,而且OnTouch執行瞭好幾次,因為OnTouch事件由DWON,MOVE,UP這三部分構成,所以onTouch執行瞭好幾次,那麼為什麼執行的順序是先onTouch後onClick呢?

 

觀察onClick和onTouch會發現onTouch()方法有返回值,默認是返回false,如果我們改為返回true,會有什麼不同,點擊打印log看看:

 

com.example.demo(3280): button touch 0
com.example.demo(3280): button touch 2
com.example.demo(3280): button touch 2
com.example.demo(3280): button touch 2
com.example.demo(3280): button touch 2
com.example.demo(3280): button touch 1

通過log結果發現Onclick事件沒有執行,我們可以理解onTouch返回true時,Button的事件被消費瞭,就相當於把view的事件攔截瞭就不會再繼續向下傳遞,因此OnClick事件沒有執行,

 

首先知道一點,隻要你觸摸到瞭界面上的任何一個控件,就一定會調用該控件的dispatchTouchEvent方法。這個方法優先於onTouch和onClick先執行,當我們去點擊按鈕的時候,就會去調用Button類裡的dispatchTouchEvent方法,那我們去Button源碼中找這個方法,Button源碼很少,沒有這個方法,Button源碼如下:

雖然沒有這個方法,但我們看出Button繼承瞭TextView,那就到TextView中取找,但是在TextView中並沒有找到dispatchTouchEvent()方法,那就隻能找TextView的父類瞭,而TextView的父類就是View對象瞭,那在View源碼中找dispatchTouchEvent()方法看看它執行邏輯:

我們首先翻譯下這個方法的說明:

@param event The motion event to be dispatched,事件動作事件派遣

@return True if the event was handled by the view, false otherwise.如果這個事件被處理瞭就返回true,否則會返回false,

現在看下dispatchTouchEvent()方法裡的代碼,看源碼得有個方法,不是所有的代碼都要看懂,

在dispatchTouchEvent()方法中重點是看

 

 if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                    mOnTouchListener.onTouch(this, event)) {
                return true;
            }

if (onTouchEvent(event)) {
                return true;
            }

 

 

首先看第一個if語句,

mOnTouchListener變量在什麼時候初始化呢?我們追蹤下,發現它的初始化時在

public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; }

因此這個是我們在setOntouchListener的時候,mOnTouchListener就可以賦值瞭,因此這個變量不會為null,

 

第二個條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷當前點擊的控件是否是enable的,按鈕默認都是enable的,因此這個條件恒定為true

if條件的第三個條件mOnTouchListener.onTouch(this, event)我們點擊onTouch()方法裡發現:

 

 

 public interface OnTouchListener {
        /**
         * Called when a touch event is dispatched to a view. This allows listeners to
         * get a chance to respond before the target view.
         *
         * @param v The view the touch event has been dispatched to.
         * @param event The MotionEvent object containing full information about
         *        the event.
         * @return True if the listener has consumed the event, false otherwise.
         */
        boolean onTouch(View v, MotionEvent event);
    }

發現它是一個接口中的方法,用於回調的,其實就是我們設置onTouchListener方法的返回值,

 

而我們返回的是true,因此這個if條件判斷返回的是true,那麼就不會執行下面的語句瞭

 

 if (onTouchEvent(event)) {
                return true;
            }

我們看看onTouchEvent(event)方法的邏輯:源碼如下:

 

 

 public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                       }

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        mPrivateFlags |= PRESSED;
                        refreshDrawableState();
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    removeTapCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            // Need to switch from pressed to not pressed
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
            return true;
        }

        return false;
    }

我們發現這方法裡面的源碼太多瞭,但是onTouchEvent()方法其實我們隻要看case MotionEvent.ACTION_UP裡面的代碼,因為我們手觸摸到最後倒是要執行這裡,在經過種種判斷之後,代碼會走到這裡:

 

 

 if (!post(mPerformClick)) {
                                    performClick();
      }

然後執行performClick()方法,performClick方法裡面的代碼:

 

 

 public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        if (mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnClickListener.onClick(this);
            return true;
        }

        return false;
    }

首先看if條件,mOnClickListener這個變量就是我們點擊的時候設置的,因此不會為null,然後我們看一個重要的方法,也是回調方法,

 

mOnClickListener.onClick(this);

這就是設置view的點擊事件,通過源碼我們現在應該明白瞭最初我們設置onClick和onTouch事件的傳遞順序,

總結:

1:如果view對象setOnTouchListener方法返回true,那麼view對象就不會執行click事件,如果setTouchListener設置為false,view才會執行click事件,

還有一個重要的知識,我們知道touch事件由DWON,MOVE,UP組成,如下:

 

btnClick.setOnTouchListener(new OnTouchListener() {
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				if(event.getAction()==MotionEvent.ACTION_DOWN){
					Log.i(com.example.demo,ACTION_DOWN);
					return false;
				}else if(event.getAction()==MotionEvent.ACTION_MOVE){
					Log.i(com.example.demo,ACTION_MOVE);	
				}else{
					Log.i(com.example.demo,ACTION_UP);
				}
				return false;
			}
		});

 

 

 

 

 

 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *