android開發藝術探索:View的事件分發機制

 

在瞭解view的時間分發機制之前,我們先了解MotionEvent這個對象

MotionEvent

在手指接觸屏幕後所產生的一系列事件中,典型的時間類型有如下幾種:

  •  ACTION_DOWN--------手機剛接觸屏幕‘
  • ACTION_MOVE----------手指在屏幕上移動
  • ACTION_UP    -----------手指從屏幕上鬆開的一瞬間

上述三種情況是典型的事件序列,同時通過MotionEvent對象我們可以得到點擊事件發生的x和y座標,爲此係統提供了兩組方法,getX/getY和getRawX/getRawY。它們的區別其實很簡單,getX/getY返回的是相對於當前view左上角的x和y座標,而getRawX/getRawY返回的是相對於手機屏幕左上角的x和y座標。

點擊事件的傳遞規則

所謂的點擊事件的事件分發,其實即使對MotionEvent事件的分發過程,即當一個MotionEvent產生以後,系統需要把這個事件傳遞給一個具體的view,而這個傳遞的過程就是分發過程。點擊事件的分發過程由三個很重要的方法來完成:

  • public boolean dispatchTouchEvent(MotionEvent ev) :用來進行事件的分發。如果事件能夠傳遞給當前view,那麼此方法一定會被調用,返回結果受當前view的ontouchEvent和下級view的dispatchTouchEvent方法的影響,表示是否消耗當前事件
    
  • public boolean onTouchEvent(MotionEvent event): 在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中(即action_down,action_move,actiong_up),當前view無法再次接收到事件,也就是說假如在action-down事件中沒有消耗,那麼action-move,action-up就不會再次觸發這個方法,只會走activity中的dispatchTouchEvent和ontouchevent方法
  • public boolean onInterceptTouchEvent(MotionEvent ev):在dispatchTouchEvent方法內部調用,用來判斷是否攔截某個事件,如果當前view攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。

上述三個方法的區別用下面的僞代碼表示:

/**
  * 點擊事件產生後
  */ 
  // 步驟1:調用dispatchTouchEvent()
  public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean consume = false; //代表 是否會消費事件

    // 步驟2:判斷是否攔截事件
    if (onInterceptTouchEvent(ev)) {
      // a. 若攔截,則將該事件交給當前View進行處理
      // 即調用onTouchEvent ()方法去處理點擊事件
        consume = onTouchEvent (ev) ;

    } else {

      // b. 若不攔截,則將該事件傳遞到下層
      // 即 下層元素的dispatchTouchEvent()就會被調用,重複上述過程
      // 直到點擊事件被最終處理爲止
      consume = child.dispatchTouchEvent (ev) ;
    }

    // 步驟3:最終返回通知 該事件是否被消費(接收 & 處理)
    return consume;

   }

當一個view需要處理事件時,如果它設置了onTouchListener,那麼OnTouchListener中的onTouch方法會被回調,這是事件如何處理還要看onTouch的返回值,如果返回flase,則當前view的OnTouchEvent方法會被調用,如果返回true,那麼此方法就不會被調用。由此可見,給view設置的OnTouchListener,其優先級比ontouchevent要高。根據源碼來看,我們平時用的onClickListener,其優先級最低。

當一個點擊事件產生後,它的傳遞順序過程遵循如下順序:

Activity-----》Window----》View

即事件總是先傳遞給activity,activity再傳給window,最後window在傳給頂級的view,頂級的view接收到事件後,就會按照時間分發機制去分發事件,及先後調用上面三個方法。考慮一種情況,如果一個view的onTouchEvent返回false,那麼他的父容器的OnTouchEvent將會調用,以此類推。也就是說 如果各個層級都不消耗事件的話,那麼從頂級view開始分發後,一直到最上面的view,這是分發,然後經過最上面view的ontouchevent方法去處理此事件,如果ontouchevent不去處理的話,就返回到他的上層的ontouchevent方法去處理,其實就是一個U型。大家慢慢去理解。

事件源碼分析

Activity對點擊事件的分發過程

當一個點擊操作發生時,事件最先傳遞給當前的activity,由activity的dispatchTouchEvent來進行事件的派發,具體的工作有activity內部的window來完成的。window會將事件傳遞給decorview,decorview一般就是當前界面的底層容器(即setcontentview 所設置的view的父容器),通過activity。getwindow。getdecorview()可以獲得,接下來看activity的dispatchtouchevent。

  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

首先事件開始交給activity所附屬的window進行分發,如果返回true,整個事件循環就結束了,返回false意味着事件沒人處理,所有view的ontouchevent都返回了false,那麼activity的ontouchevent就會被調用。其實window分發最後是到了頂層的view,中間過程就不詳細說了。

viewGroup對點擊事件的分發過程

點擊事件達到頂級view(一般是一個viewGroup)以後,會調用viewgroup的diapatchtouchevent方法,如果viewGroup攔截事件即onInterceptTouchEvent返回true,則事件由viewGroup處理,這是如果viewGroup的ontouchlistener被設置了,則onTouch會被調用,如果onTouch返回true,就會屏蔽掉onTouchEvent,如果返回false,會接着執行OnTouchEvent方法,好了  下面我們看一下dispatchtouchevent方法的源碼:

  // Check for interception.
    final boolean intercepted;
    //這裏檢查是否攔截事件
    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;
    }

ViewGroup在兩種情況下都會判斷是否要攔截當前事件

  • 事件類型爲ACTION_DOWN:當前由我們觸發的點擊事件,也即是說ACTION_MOVE和ACTION_UP事件來時,則不觸發攔截事件
  • mFirstTouchTarget != null:當ViewGroup不攔截事件並將事件交給子View的時候該不等式成立。反過來,事件被ViewGroup攔截時,該不等式不成立

然後接着看viewgroup遍歷子view:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    ......

    final View[] children = mChildren;
    //遍歷所有子View
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

        //判斷子View是否能接收點擊事件
        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);
        //事件傳遞到子View,下面追蹤該方法
        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);
    }

    ......
}

ViewGroup直接使用for遍歷所有子View,對子View的各種狀態進行判斷,最後調用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)將事件傳遞給子View,下面是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的dispatchTouchEvent()方法,在這裏,我有一些不明白的地方,但是經過demo測試,如果嵌套了好幾個viewgroup類型的view,那麼就會多次執行viewgroup的dispatchTouchEvent()方法,如果最上層是一個view的話,比如textivew,那麼就會走view的dispatchTouchEvent(),那麼接下里就會進入view的事件分發,我自己感覺,也就是說,如果說嵌套的佈局裏面沒有view(類似textview這一類),那麼就不會走view的dispatchTouchEvent,只能走viewgroup的dispatchTouchEvent,不知道這樣理解對不對,希望看到的大佬給個建議

view對點擊事件的分發過程

view的dispatchTouchEvent源碼分析

public boolean dispatchTouchEvent(MotionEvent event) {

    boolean result = false;
    ......
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //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;
}

從源碼判斷處看出,首先會判斷有沒有設置mOnTouchListener,如果mOnTouchListener不爲空,那麼onTouchEvent就不會被調用,這裏可以得到一個結論,若在View中設置了OnTouchListener,那麼它的優先級是高於onTouchEvent的,這樣可以更好的讓我們自己setOnTouchEventListener()處理點擊事件

onTouchEvent源碼處理事件的具體做法部分

public boolean onTouchEvent(MotionEvent event) {
    ......
    //當View處於不可用狀態下,也會消耗點擊事件
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // 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)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

    ......
    //對點擊事件的具體處理
    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) {
                    ......
                    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;
    }
    ......
}

從對點擊事件的具體處理中看出,只要View的CLICKABLE和LONG_CLICKABLE有一個爲true,那麼它就會消耗這個事件,即onTouchEvent方法返回true。在ACTION_UP事件中,會觸發PerformClick()方法,如果View設置了OnClickListener,那麼PerformClick()方法內部會調用它的onClick()方法

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