Android View體系(五)從源碼解析View的事件分發機制

來源:互聯網
上載者:User

標籤:

相關文章
Android View體系(一)視圖座標系
Android View體系(二)實現View滑動的六種方法
Android View體系(三)屬性動畫
Android View體系(四)從源碼解析Scroller

前言

三年前寫過事件分發機制的文章但是寫的不是很好,所以重新再寫一篇,關於事件分發機制的文章已經有很多,但是希望我這篇是最簡潔、最易懂的一篇。

1.處理點擊事件的方法 View的層級

我們知道View的結構是樹形的結構,View可以放在ViewGroup中,這個ViewGroup也可以放到另一個ViewGroup中,這樣層層的嵌套就組成了View的層級。

什麼是點擊事件分發

當我們點擊螢幕,就產生了觸摸事件,這個事件被封裝成了一個類:MotionEvent。而當這個MotionEvent產生後,那麼系統就會將這個MotionEvent傳遞給View的層級,MotionEvent在View的層級傳遞的過程就是點擊事件分發。

點擊事件分發的重要方法

點擊事件有三個重要的方法它們分別是:

  • dispatchTouchEvent(MotionEvent ev):用來進行事件的分發
  • onInterceptTouchEvent(MotionEvent ev):用來進行事件的攔截,在dispatchTouchEvent()中調用,需要注意的是View沒有提供該方法
  • onTouchEvent(MotionEvent ev):用來處理點擊事件,在dispatchTouchEvent()方法中進行調用

為了瞭解這三個方法的關係,我們先來看看ViewGroup的dispatchTouchEvent()方法的部分源碼:

 @Override    public boolean dispatchTouchEvent(MotionEvent ev) {       ...省略            if (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != null) {                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                if (!disallowIntercept) {                    intercepted = onInterceptTouchEvent(ev);                    ev.setAction(action); // restore action in case it was changed                } else {                    intercepted = false;                }            } else {                // There are no touch targets and this action is not an initial down                // so this view group continues to intercept touches.                intercepted = true;            }           ...省略        return handled;    }

很明顯在dispatchTouchEvent()方法中調用了onInterceptTouchEvent()方法來判斷是否攔截事件,來看看onInterceptTouchEvent()方法:

 public boolean onInterceptTouchEvent(MotionEvent ev) {        return false;    }

onInterceptTouchEvent()方法預設返回false,不進行攔截,再接著來看看dispatchTouchEvent()方法的下面的部分源碼:

 public boolean dispatchTouchEvent(MotionEvent ev) { ...省略              final View[] children = mChildren;              for (int i = childrenCount - 1; i >= 0; i--) {                            final int childIndex = customOrder                                    ? getChildDrawingOrder(childrenCount, i) : i;                            final View child = (preorderedList == null)                                    ? children[childIndex] : preorderedList.get(childIndex);                            // If there is a view that has accessibility focus we want it                            // to get the event first and if not handled we will perform a                            // normal dispatch. We may do a double iteration but this is                            // safer given the timeframe.                            if (childWithAccessibilityFocus != null) {                                if (childWithAccessibilityFocus != child) {                                    continue;                                }                                childWithAccessibilityFocus = null;                                i = childrenCount - 1;                            }                            if (!canViewReceivePointerEvents(child)                                    || !isTransformedTouchPointInView(x, y, child, null)) {                                ev.setTargetAccessibilityFocus(false);                                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;                            }                            resetCancelNextUpFlag(child);                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                                // Child wants to receive touch within its bounds.                                mLastTouchDownTime = ev.getDownTime();                                if (preorderedList != null) {                                    // childIndex points into presorted list, find original index                                    for (int j = 0; j < childrenCount; j++) {                                        if (children[childIndex] == mChildren[j]) {                                            mLastTouchDownIndex = j;                                            break;                                        }                                    }                                } else {                                    mLastTouchDownIndex = childIndex;                                }                                mLastTouchDownX = ev.getX();                                mLastTouchDownY = ev.getY();                                newTouchTarget = addTouchTarget(child, idBitsToAssign);                                alreadyDispatchedToNewTouchTarget = true;                                break;                            }                            // The accessibility focus didn‘t handle the event, so clear                            // the flag and do a normal dispatch to all children.                            ev.setTargetAccessibilityFocus(false);                        } ...省略}

我們看到了for迴圈,首先遍曆ViewGroup的子項目,判斷子項目是否能夠接收到點擊事件,如果子項目能夠接收到則交由子項目來處理。接下來看看dispatchTransformedTouchEvent()方法中實現了什麼:

 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,            View child, int desiredPointerIdBits) {        final boolean handled;        // Canceling motions is a special case.  We don‘t need to perform any transformations        // or filtering.  The important part is the action, not the contents.        final int oldAction = event.getAction();        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {            event.setAction(MotionEvent.ACTION_CANCEL);            if (child == null) {                handled = super.dispatchTouchEvent(event);            } else {                handled = child.dispatchTouchEvent(event);            }            event.setAction(oldAction);            return handled;        }  ...省略       }       

如果有子View則調用子View的dispatchTouchEvent(event)方法。如果ViewGroup沒有子View則調用super.dispatchTouchEvent(event),ViewGroup是繼承View的,我們再來看看View的dispatchTouchEvent(event):

 public boolean dispatchTouchEvent(MotionEvent event) {       ...省略        boolean result = false;        if (onFilterTouchEventForSecurity(event)) {            //noinspection SimplifiableIfStatement            ListenerInfo li = mListenerInfo;            if (li != null && li.mOnTouchListener != null                    && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {                result = true;            }            if (!result && onTouchEvent(event)) {                result = true;            }        }     ...省略        return result;    }

我們看到如果OnTouchListener不為null並且onTouch()方法返回true,則表示事件被消費,就不會執行onTouchEvent(event),否則就會執行onTouchEvent(event)。再來看看onTouchEvent()方法的部分源碼:

 public boolean onTouchEvent(MotionEvent event) {      ...省略        final int action = event.getAction();        if (((viewFlags & CLICKABLE) == CLICKABLE ||                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {            switch (action) {                case MotionEvent.ACTION_UP:                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {                        // take focus if we don‘t have it already and we should in                        // touch mode.                        boolean focusTaken = false;                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {                            // 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();                                }                            }                        }       ...省略            }        return true;       }                 return false;    }

上面可以看到只要View的CLICKABLE和LONG_CLICKABLE一個為true,那麼onTouchEvent就會返回true消耗這個事件。CLICKABLE和LONG_CLICKABLE代表View可以被點擊和長按點擊,可以通過View的setClickable和setLongClickable方法來設定,也可以通過View的setOnClickListenter和setOnLongClickListener來設定,他們會自動將View的設定為CLICKABLE和LONG_CLICKABLE。
接著在ACTION_UP事件會調用performClick()方法:

    public boolean performClick() {        final boolean result;        final ListenerInfo li = mListenerInfo;        if (li != null && li.mOnClickListener != null) {            playSoundEffect(SoundEffectConstants.CLICK);            li.mOnClickListener.onClick(this);            result = true;        } else {            result = false;        }        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);        return result;    }

如果View設定了點擊事件OnClickListener,那麼它的onClick()方法就會被執行。

2.點擊事件分發的路由規則

看到這裡我們就可以知道點擊事件分發的這三個重要方法的關係,用虛擬碼來簡單表示就是:

 public boolean dispatchTouchEvent(MotionEvent ev) { boolean result=false; if(onInterceptTouchEvent(ev)){       result=super.onTouchEvent(ev);  }else{       result=child.dispatchTouchEvent(ev); } return result;
點擊事件由上而下的路由規則

當點擊事件產生後會由Activity來處理在傳遞給Window再傳遞給頂層的ViewGroup,一般在事件傳遞中只考慮ViewGroup的onInterceptTouchEvent()方法,因為一般情況我們不會去重寫dispatchTouchEvent()方法。
對於根ViewGroup,點擊事件首先傳遞給它的dispatchTouchEvent()方法,如果該ViewGroup的onInterceptTouchEvent()方法返回true,則表示它要攔截這個事件,這個事件就會交給它的onTouchEvent()方法處理,如果onInterceptTouchEvent()方法返回false,則表示它不攔截這個事件,則交給它的子項目的dispatchTouchEvent()來處理,如此的反覆下去。如果傳遞給最底層的View,View是沒有子View的,就會調用View的dispatchTouchEvent()方法,一般情況下最終會調用View的onTouchEvent()方法。

舉個現實的例子,就是我們的應用產生了重大的bug,這個bug首先會彙報給技術總監那:

技術總監(頂層ViewGroup)→技術經理(中層ViewGroup)→工程師(底層View)
技術總監不攔截,把bug分給了技術經理,技術經理不攔截把bug分給了工程師,工程師沒有下屬只有自己處理了。
事件由上而下傳遞傳回值規則為:true,攔截,不繼續向下傳遞;false,不攔截,繼續向下傳遞。

點擊事件由下而上的路由規則

點擊事件傳給最底層的View,如果他的onTouchEvent()方法返回true,則事件由最底層的View消耗並處理了,如果返回false則表示該View不做處理,則傳遞給父View的onTouchEvent()處理,如果父View的onTouchEvent()仍舊返回返回false,則繼續傳遞給改父View的父View處理,如此的反覆下去。

再返回我們現實的例子,工程師發現這個bug太難搞不定(onTouchEvent()返回false),他只能交給上級技術經理處理,如果技術經理也搞不定(onTouchEvent()返回false),那就把bug傳給技術總監,技術總監一看bug很簡單就解決了(onTouchEvent()返回true)。

事件由下而上傳遞傳回值規則為:true,處理了,不繼續向上傳遞;false,不處理,繼續向上傳遞。

點擊事件傳遞時的其他問題
  • 上面源碼我們看到:如果我們設定了OnTouchListener並且onTouch()方法返回true,則onTouchEvent()方法不會被調用,否則則會調用onTouchEvent()方法,可見OnTouchListener的優先順序要比onTouchEvent()要高。在OnTouchEvent()方法中,如果當前設定了OnClickListener則會執行它的onClick()方法。
  • View的OnTouchEvent()方法預設都會返回true,除非它是不可點擊的也就是CLICKABLE和LONG_CLICKABLE都為false。

Android View體系(五)從源碼解析View的事件分發機制

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.