Android事件分發、View事件Listener全解析

     轉載請聲明出處http://blog.csdn.net/zhongkejingwang/article/details/38141719

問題

在自定義控件和設置事件Listener的時候,很多人想當然,完全根據自己的意願處理返回值,比如down事件我不想處理,我要交給view自己處理所以return false或者return super.onTouchEvent(event);;我要自己處理move事件,不想交給view處理所以return true。這些想法給自己也給別人帶來了很多bug。很多android開發者會遇到這樣的問題:

    爲什麼我在自定義view的onTouchEvent中處理了touch事件後我的onClickListener,onLongClickListener沒有執行了?

    爲什麼onTouchEvent中down的時候return false就收不到後面的事件了?而在move的時候返回return false爲什麼還可以收到up事件?

    爲什麼我自定義的view會誤觸發LongClick事件和Click事件?

    爲什麼我給view設置了onTouchListener處理事件後會誤觸發LongClick事件和Click事件?

    onTouchEvent,onTouchListener,onClickListener,onLongClickListener這些方法和接口在什麼時候什麼地方會被調用?

    。。。。。。

這些典型的事件衝突問題,在看完這篇文章之後,你就知道這些問題的答案了。文章中貼的源碼是android4.0的,看有中文註釋的地方就行了。

ViewGroup事件分發

     首先從ViewGroup開始講。平時我們使用到的xxxLayout都是繼承自ViewGroup,ViewGroup擔任着事件分發者的角色,將TouchEvent分發給Layout中的子View,它自己也可以像View一樣處理無View認領的事件,因爲ViewGroup的父類是View。ViewGroup中分發事件的方法是dispatchTouchEvent。當一個TouchEvent產生的時候,當前Activity會將該event交給位於最外層的xxxLayout,xxxLayout中的dispatchTouchEvent得到執行,在dispatchTouchEvent中將event交給包含該touch區域的子View。除了Activity中最外層的layout,所有View獲得的事件序列的開始都是down事件。要了解事件分發的過程,就需要看ViewGroup的dispatchTouchEvent源碼了,下面的源碼講解是按順序的,爲了方便講解,代碼有省略,如果想要看完可以自己去找來看。
首先進入dispatchTouchEvent:
            ......
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
	......
    可以看到,down事件到來的時候會重置所有狀態並清空Target list,這個list是用來存放處理了event的子view的target的,暫時不用理會。繼續,接着看下面的,注意兩個變量newTouchTargetalreadyDispatchedToNewTouchTarget
......
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
			......
			//遍歷子view
			for (int i = childrenCount - 1; i >= 0; i--) {
                            final View child = children[i];
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
			......
			//如果child的dispatchTouchEvent返回true,dispatchTransformedTouchEvent也會返回true,這時候條件就會成立,newTouchTarget和alreadyDispatchedToNewTouchTarget就會被修改。這裏傳進去child,方法裏就會調用child.dispatchTouchEvent,如果傳null就會調用super.dispatchTouchEvent
			if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                mLastTouchDownIndex = i;
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //將處理了down事件的view的target添加到target list的頭部,此時newTouchTarget和mFirstTouchTarget是相等的
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //標記事件已經分發給子view
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
			}
		}
	}
	......
先不管事件攔截啥的,看這段代碼,就是看這兩個變量newTouchTarget和alreadyDispatchedToNewTouchTarget在過了這段代碼後的值有沒有改變,在down的時候可以進入這裏面,所以如果子view的dispatchTouchEvent如果返回false的話該子view的target就不會被添加到target list中了,兩個變量的值沒有被改變。但在move和up的時候就進不去這裏,所以在move和up事件時newTouchTarget是null的,alreadyDispatchedToNewTouchTarget是false。好的,接着往下
       ......
	//mFirstTouchTarget是一個全局變量,指向target list的第一個元素。
	 // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
		//進入這裏,說明前面down事件處理的子view返回false,導致沒有target被添加到list中,也就是這個事件沒有view認領。
                // No touch targets so treat this as an ordinary view.
		//這裏有個參數傳了null,方法裏面會判斷這個參數,如果爲null就調用super.dispatchTouchEvent,也就是自己來處理event。由於down後面的事件都沒法修改mFirstTouchTarget,所以之後的事件都在這裏執行了,該子view就沒法接收到後面的事件了
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
		//能進入這裏,說明子view在處理down事件後返回了true。後面的move和up事件會直接進入這裏
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
		//這裏遍歷這個target list,挨個分發事件,爲了方便理解,可以暫時認爲list裏面此時就只包含一個target的,也就是當前處理事件的view的target
                    final TouchTarget next = target.next;
		//如果是down事件的話,就會進入這個if裏面,由於down事件在前面已經處理了,所以直接handled = true。因爲如果是move和up的話alreadyDispatchedToNewTouchTarget是false,newTouchTarget是null。
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
			//handled會被返回到當前Activity的dispatchTouchEvent中,具體在Activity中怎麼使用可以查看其源碼
                        handled = true;
                    } else {
			//move和up事件會進入這裏
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
			//看這裏,由此可知在move的時候返回true或者false只會影響到layout返回給Activity的值,由於不是down事件所以不會影響up事件的獲取。
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
	......
註釋已經寫的很明白了,在註釋中已經回答了一開始的幾個問題了,處理down事件和處理down後面的事件有很大的區別,只要記住事件序列是以down開始的就可以了。

事件分發總結:

如果down事件沒有被子view處理或者子view處理down事件後返回false,那麼ViewGroup自己處理,也就是用父類View的代碼處理,若父類給自己返回false的話,那麼Activity不會把後面的move和up事件分發給ViewGroup了。
如果某一子view處理down事件並返回true,那麼將該子view記錄下來。後面move和up事件到來時直接將事件交給處理了down事件的該子view。

OK,既然事件已經分發至view中了。那我們就開始進入view中的事件處理講解吧~

View事件處理

 前面layout把事件分發到這裏了,就算沒有字view處理,layout也會交給父類也就是View處理,依然是這裏。在View事件處理中,將會講到View的dispatchTouchEvent,onTouchListener,onClickListener,onLongClickListener這些個方法和監聽器的執行和回調時間點。前面看到,ViewGroup把event分給View的dispatchTouchEvent,現在,就從這裏開始講。
      進入到View中:
public boolean dispatchTouchEvent(MotionEvent event) {
		......
	    //看這裏吧,如果設置了onTouchListener就在這裏回調的
            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                    mOnTouchListener.onTouch(this, event)) {
                return true;
            }
	    //再來看這裏,調用了onTouchEvent,onClickListener和onLongClickListener就在onTouchEvent裏面回調的
            if (onTouchEvent(event)) {
                return true;
            }
		......
        return false;
    }
我把其他暫時不用考慮的省略了,突出重點方便理解。看到View的dispatchTouchEvent,我們就可以開始回答前面的一些問題了:
假如我給View設置了onTouchListener並且在onTouch處理後返回true,那麼onTouchEvent就不會被執行了,也就是說onClickListener和onLongClickListener都不會被回調了。
假如我在onTouchListener的onTouch中處理down的時候返回false,move和up返回值先不用管,這時候如果move事件持續事件長的話會觸發長按事件,長按事件不觸發就會觸發點擊事件。

具體爲什麼,還要繼續往下看,下面進入View的重頭戲onTouchEvent中看看:
public boolean onTouchEvent(MotionEvent event) {
	......
	//從if條件可以看到,基本的View在onTouchEvent中只處理click事件和longclick事件
	if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
						......
                        if (!mHasPerformedLongPress) {
			    //能進入這裏說明up的時候長按事件還沒有被執行
                            // This is a tap, so remove the longpress check,將長按回調從消息列表刪除
                            removeLongPressCallback();
								......
                                // 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();
                                }
			        //用PerformClick異步執行,若不成功再調用performClick()
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            ......
                        }
                        ......
                    break;

                case MotionEvent.ACTION_DOWN:
		    //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.
		    //這裏判斷父控件是否是一個可滾動的控件,如果是,長按事件的回調會被延長,CheckForTap類是實現了Runnable接口內部類,在其run方法中會執行checkForLongClick方法,將長按的回調放入handle消息列表中,一段時間後執行
                    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();
		        //將長按回調放入消息列表,傳入0表示經過默認時間後執行長按事件回調
                        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,手指移出view區域時會移除消息列表中的所有回調,包括長按的回調
                        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;
        }
        //如果一個View不可點擊也不可長按,這裏返回false會導致dispatchTouchEvent返回false
        return false;
		}
從源碼可以看到,down事件到來的時候,View就開始爲LongClick計時了,所以說,在開始計時後如果沒有接收到後續事件的話,長按事件就會觸發,這時候mHasPerformedLongPress被賦值爲true,click事件就不會被執行了,如果在計時結束之前,傳進來up事件的話,mHasPerformedLongPress還是false,長按回調消息被移除,執行click回調。到這裏,已經可以回答前面的事件衝突問題了:
假如我自定義了View,複寫了onTouchEvent,返回值是true或者false,並沒有調用父類的代碼,這時候設置了Listener當然不會被調用了,除非自己在這裏面手動執行performLongClick()或performClick()。
假如我自定義了View,複寫了onTouchEvent,在down處理完後又想將事件交給父類處理所以return super.onTouchEvent;接下來的move事件我不想給父類處理了,直接return true或false,這時候過一會兒就會觸發長按事件;假如我在長按事件觸發前處理完了up事件return super.onTouchEvent,這時候就會觸發click事件。
這是在沒有設置onTouchListener情況下的誤觸發,在設置了onTouchListener時的誤觸發這兩個事件道理是一樣的。要解決這些問題,可以參照view的處理方式,在覆蓋父類方法的情況下手動執行回調。有時候使用別人寫的控件的時候別人可能沒有處理這些Listener,這時候怎麼設置Listener都不會被回調的,需要自己修改。有時候自己並不想觸發長按事件,那就想辦法把回調消息刪掉,可以用通過反射刪除,後面講仿QQ下拉刷新的時候會用到。
如果想要更清楚的瞭解android的事件分發機制,去仔細看源碼吧。。。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章