Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android事件處理(一)——ViewGroup的dispatchTouchEvent 函數源碼詳解

Android事件處理(一)——ViewGroup的dispatchTouchEvent 函數源碼詳解

編輯:關於Android編程

文章意圖:

主要是想一自己閱讀代碼後的一些小收獲分享給大家。讓大家更加深入的了解Android的事件分發這塊的內容。

文章主要內容:

本文將在第一章通過自己的語言,簡單介紹dispatchTouchEvent,並將其中的一些關鍵點直接提煉出來,方便那些不想閱讀源代碼的同學把握住其中的關鍵點。

在第二章將放上源碼,其中包含了我閱讀過程中的26處注釋。

第一章、dispatchTouchEvent關鍵點提煉

ViewGroup的dispatchTouchEvent是對View的dispatchTouchEvent函數的重新,兩者具有很大的區別。如下表:

View的dispatchTouchEvent函數 ViewGroup的dispatchTouchEvent函數 1)是將Event派發給自己的onTouchEvent函數處理。

1)將Event派發給子View

2)只有在子View沒有相應該事件或ViewGroup攔截了該事件的時候,才通過View的dispatchTouchEvent將事件派發給自身。

 

有上面兩個表,應該大致了解了ViewGroup的dispatchTouchEvent大致講上面了吧。那現在具體講講做了哪些事: Step 1: 如果收到的ACTION_DOWN 那麼清除所有和時間相關的狀態 Step 2: 判斷是否攔截事件

 

1) 如果在子view中調用了getParent().requestDisallowInterceptTouchEvent(true)那麼將不會攔截該事件。
問:有什麼用呢?答:在listView或其他srcollView都會在onInterceptTouchEvent進行攔截判斷,只要距離或者速度達到一定要求,就會攔截該事件。那就做不到一view在listView中上下滑動。那麼如何才能實現這個需求?那就需要使用getParent().requestDisallowInterceptTouchEvent(true)不要攔截該事件,將事件傳遞下去。
2)onInterceptTouchEvent() 函數這是一個很重要的函數,是在當前的事件達到一定要求之後才進行攔截處理。有了這個,listView才能實現item可點擊,而item上下滑動。

Step 3: 如果事件是ACTION_DOWN ,並且沒有被攔截,將會執行向子View的派發處理。

1)派發順序

5.0之前,基本按照子View被添加的順序

5.0之後,還需要考慮Z軸、Drawing順序、添加的順序

重點關注Z軸順序,值最大的將會最優先,這是得到所有的子view,那麼如何判斷是否點擊到子View區域內呢?

2)判斷子View是否可見或者存在動畫,否則不處理

3)事件是否點擊到子View的區域內

 
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        //[lxb] 如果view做了相應的變換,那麼將point也做相應的變換
        transformPointToViewLocal(point, child);
        //[lxb] 直接將pointInView函數拿出
        //  return LocalX >= 0 && localX < (mRight - mLeft)
        //        && localY >= 0 && localY < (mBottom - mTop);
        // 經過變換之後的point,則已經將左上角作為原點,所以只要判斷是否在這個矩形中就可以了
        // 所以這就是為什麼如TranslateAnimation的原始點擊點還是在最開始的地方
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }
這也就是為什麼補間動畫盡管顯示的View已經移動了另一個地方,但是點擊區域還是在最原始的地方。

 

4)派發該事件給子View,看子View是否要處理

注意,parent將event該子view的時候做了相應的偏移處理,所以子view中的event.getX() 和 parent中的不一樣。

具體如下:

	//[lxb] 將event做相應的偏移之後,然後傳遞給相應的子view
	final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        event.offsetLocation(offsetX, offsetY);

        handled = child.dispatchTouchEvent(event);

5)將相應了該事件的子view加入到mFirstTouchTarget的單鏈表中(接下來的事件都會直接派發給其中的view)

Step 4: 如果沒有一個子View相應Action_DOWN 怎麼辦?

那麼直接將事件交給父類View的dispatchTouchEvent,再根據條件派發給自己。

Step 5: 其他事件如 ACTION_MOVE, ACTION_UP, ACTION_CANCEL則直接照著mFirstTouchTarget鏈表派發就行。

最後:

講講攔截,如果ViewGrop 攔截該事件,那麼將立馬給之前相應事件的子View派發一個ACTION_CANCEL事件。

另外,還需要注意的是,如果ViewGrop 攔截該事件,並不會立馬將該事件給自己的onTouchEvent,只有等下次事件過來才可能,所以在onInterceptTouchEvent中需要對ACTION_UP和ACTION_CANCEL也做相應處理。

第二章、dispatchTouchEvent源碼及分析

 public boolean dispatchTouchEvent(MotionEvent ev) {
        //[lxb #1] 調試使用,請忽略
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        //[lxb #2] [輔助功能] 事件將會第一個派發給開啟了accessibility focused的view
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

        boolean handled = false;
        //[lxb #3] 表示窗口是否為模糊窗口(FILTER_TOUCHES_WHEN_OBSCURED),如果是窗口,則表示不希望處理改事件。(如dialog後的窗口)
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            //[lxb #4] 過濾字段的最後8bit,也就是指只關心是ACTION_DOWN、ACTION_UP等事件,而不關心是哪個手指引起的。
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 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.
                //[lxb #5] 初始化相關狀態
                //[lxb #5] (01) 清空mFirstTouchTarget鏈表,並設置mFirstTouchTarget為null。mFirstTouchTarget是"接受觸摸事件的View"所組成的單鏈表
                //[lxb #5] (02) 清空mGroupFlags的FLAG_DISALLOW_INTERCEPT標記,如果設置了FLAG_DISALLOW_INTERCEPT,ViewGroup對觸摸事件進行攔截。
                //[lxb #5] (03) 清空mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVEN標記,作用是將下一個時間變我Cancel
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            //[lxb #6] 如果為DOWN事件,或者mFirstTouchTarget為null(那麼事件直接給到自己),就沒必要執行攔截。
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //[lxb #7] 查看是否設置了,禁止攔截的標記
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //[lxb #8] ViewGroup的onInterceptTouchEvent不執行攔截,除非子類重寫了該方法(如listview)
                    intercepted = onInterceptTouchEvent(ev);
                    //[lxb #9] 僅僅是避免action被篡改過。
                    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;
            }

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
            //[lxb #10] 查看時候被標記了PFLAG_CANCEL_NEXT_UP_EVENT 或者 當前是一個Cancel事件
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            //[lxb #11] 比如我們多個手指放到了屏幕上,是否要將第二個手指的事件下面下去
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {

                // If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    //[lxb #12] 清除Targets中相應的pointer ids 
                    removePointersFromTouchTargets(idBitsToAssign);

                    //[lxb #13] 遍歷所有的child,將事件派發下去
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        //[lxb #13] 以 1)Z軸(5.0系統引入) 2)draw的順序 進行排序
                        final ArrayList preorderedList = buildOrderedChildList();

                        //[lxb #14] 可以理解為,是否按照draw的順序(因為,buildOrderedChildList在都沒有設置Z的情況下返回null)
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //[lxb #15] 這裡兩端代碼,簡單的理解根據不同的排列選項(1、view添加到 2、view的draw順序 3、viewZ 軸順序)
                            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.
                            //[lxb #16] 如果存在開啟了AccessibilityFocus 的view
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                //[lxb #17] 如果正是當前的childView開啟了AccessibilityFocus,直接將i指向最後一個元素
                                //[lxb #17] 和 break的區別是,還將執行後面的代碼,但是不會再進行循環了
                                i = childrenCount - 1;
                            }
                            //[lxb #18] canViewReceivePointerEvents 判斷child是否為visiable 或者 是否有動畫
                            //[lxb #18] isTransformedTouchPointInView 判斷x, y是否在view的區域內(如果是執行了補間動畫 則x,y會通過獲取的matrix變換值
                            //[lxb #18] 換算當相應的區域,這也是為什麼補間動畫的觸發區域不隨著動畫而改變)
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            //[lxb #19] getTouchTarget 查找child是否已經記錄在mFirstTouchTarget這個單鏈表中
                            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);
                            //[lxb #20] 簡單的理解,dispatchTransformedTouchEvent就是將相應的事件傳遞下去
                            //[lxb #20] 不過需要注意一點的就是,event被傳遞給child的時候將會做相應偏移,如下
                            //[lxb #20] final float offsetX = mScrollX - child.mLeft;
                            //[lxb #20] final float offsetY = mScrollY - child.mTop;
                            //[lxb #20] event.offsetLocation(offsetX, offsetY);
                            //[lxb #20] 為什麼要做偏移呢? 因為event的getX得到的值是,childView到parentView邊境的距離,是一個相對值
                            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
                                    //[lxb #21] 找到childIndex所代表的child的最原始的index【?】看代碼,children和mChildren指向同一鏈表
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //[lxb #22] 將相應該事件的child包裝成一個Target,添加到mFirstTouchTarget的鏈表中
                                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);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    //[lxb #22] 如果沒有child相應該事件,則將此事件交給最近加入的target
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            //[lxb #23] mFirstTouchTarget == null 表示,沒有能相應該事件的child,那麼就調用父類(也就是View)的dispatchTouchEvent
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // 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) {
                    final TouchTarget next = target.next;
                    //[lxb #24] 表示在Down事件處理中,已經將這個事件交給newTouchTarget處理過了,就不重復處理了
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        //[lxb #25] 再次判定是否需要cancel(被標記為cancel 或者 事件被攔截)
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        //[lxb #26] 問:難道看到這裡你們會不會產生一個疑問,如果parent在ACTION_MOVE過程中攔截了該事件,哪裡hi處理呢?
                        //[lxb #26] 答:如果攔截了該事件,還是需要自身 dispatchTransformedTouchEvent 函數將事件交個自己的onTouchEvent
                        //[lxb #26]     此外dispatchTransformedTouchEvent完成上述操作需要一個條件,也就是child形參數為null
                        //[lxb #26] 問:那麼怎麼為null呢?
                        //[lxb #26] 答:往上面看10幾行,不就是了嗎?
                        //[lxb #26] 那麼如何達到,其實條件就是 mFirstTouchTarget == null, 請看下面的分析
                        //[lxb #26]     如果intercepted == true的情況下, cancelChild == true, predecessor == null
                        //[lxb #26] 從而使得mFirstTouchTarget 一直 -> next,當target遍歷到最後的時候,next == null,從而使得mFirstTouchTarget == null。
                        //[lxb #26] 問: 稍等,這裡僅僅做了將mFirstTouchTarget 設置了為null,那麼如何派發給自己的onTouchEvent呢?
                        //[lxb #26] 這個只能等下一個事件過來了
                        //[lxb #26] 結論【事件攔截,攔截了該事件,並沒有將本次這個事件傳遞給自身的onTouchEvent,而需要等到下次】
                        //[lxb #26] 問:如何驗證
                        //[lxb #26] 答:重新FrameLayout的 onInterceptTouchEvent 和 onTouchEvent 將相應的event.getEventTime打印出來,
                        //[lxb #26]     將會發現攔截的事件和傳遞到onTouchEvent的時間不是一個時間。
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            //[lxb #27] cancel ACTION_UP  ACTION_HOVER_MOVE(表示鼠標滑動)等,清理狀態
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            }else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        //[lxb #28] 調試使用,可以忽略 
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved