View—事件分發

TouchTarget

在開始分析事件分發以前, 須要瞭解一下 TouchTarget 這個類的做用
在ViewGroup的事件分發中, 它起到了不可獲取的做用, 不瞭解其原理, 看起源碼可能會致使身體不適
下面開始分析bash

/**
    * ViewGroup.TouchTarget 鏈表結構
    */
    private static final class TouchTarget {
        // 鏈表最大的長度
        private static final int MAX_RECYCLED = 32;
        // 用於控制同步的鎖
        private static final Object sRecycleLock = new Object[0];
        // sRecycleBin 內部可複用實例鏈表表頭
        // 注意 ViewGroup 中還維護着一個 mFirstTouchEvent, 它是外部記錄正在響應事件 View 的鏈表, 響應完成以後會調用 recycler 方法, 加入 sRecycleBin 這個可複用的鏈表中
        private static TouchTarget sRecycleBin;
        // 內部可複用的實例鏈表的長度
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // 當前被觸摸的 View
        public View child;
        // The combined bit mask of pointer ids for all pointers captured by the target.
        // 對目標捕獲的全部指針的指針id的組合位掩碼
        public int pointerIdBits;
        // 鏈表中指向的下一個目標
        public TouchTarget next;

        private TouchTarget() {
        }

        public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
            if (child == null) {
                throw new IllegalArgumentException("child must be non-null");
            }

            final TouchTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new TouchTarget();
                } else {
                    // 從鏈表複用池中取出一個對象, 並重至屬性值
                    target = sRecycleBin; // 將當前的表頭賦給這個變量
                    sRecycleBin = target.next; // 表頭移動到下個位置
                    sRecycledCount--; // 當前複用池的數量 -1 
                    target.next = null; // 將它的 next 置空
                }
            }
            // 從新綁定數據
            target.child = child;
            target.pointerIdBits = pointerIdBits;
            return target;
        }
        
        public void recycle() {
            if (child == null) {
                throw new IllegalStateException("already recycled once");
            }
            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    // 以前的表頭變成了 next
                    next = sRecycleBin;
                    // 更新鏈表表頭位置爲本身
                    sRecycleBin = this;
                    // 複用池的數量 +1
                    sRecycledCount += 1;
                } else {
                    next = null;
                }
                child = null;
            }
        }
    }
    
    
    /**
     * ViewGroup.addTouchTarget
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        // 1. 建立一個新的 TouchTarget 對象
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        // 2. 讓他鏈上上一個對象, 第一次的話 mFirstTouchTarget 確定爲 null
        target.next = mFirstTouchTarget;
        // 3. 更新 mFirstTouchTarget 的值
        mFirstTouchTarget = target;
        return target;
    }
    
    /**
     * ViewGroup.getTouchTarget
     */
    private TouchTarget getTouchTarget(@NonNull View child) {
        // 變量 mFirstTouchTarget 造成的鏈表, 尋找當前 child 對應的 TouchTarget
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }
複製代碼

須要着重注意的兩點post

  1. sRecycleLock: 在 TouchTarget 內部維護的鏈表, 用於處理 TouchTarget 實例的複用
    • sRecycleLock 指向鏈表首部
  2. mFirstTouchTarget: 在 ViewGroup 中維護的鏈表, 用於記錄當前響應事件序列的子 View (一個事件序列對應一個響應它的子View)
    • mFirstTouchTarget 指向鏈表首部
    • 當其綁定的 View 消費了當前的事件以後, 會調用 TouchTarget.recycle 從 mFirstTouchTarget 所在鏈表中移除且回收進 sRecycleLock 所在鏈表中

分析 ViewGroup 對事件的分發

/**
     * ViewGroup.dispatchTouchEvent
     * mFirstTouchTarget 是當前正在響應事件鏈表的表頭
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 1. 若爲 ACTION_DOWN 則清除以前的狀態, ACTION_DOWN 爲整個序列事件的初始化事件
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 在開始一個新的觸摸手勢時,拋棄全部之前的狀態。
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
        // 2.  判斷是否須要當前 ViewGroup 攔截事件
        // 2.1 DOWN 事件會判斷是否須要攔截
        // 2.2 當前事件序列已經以前有子 View 響應(mFirstTouchTarget != null) 判斷是否須要攔截
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 調用本身的 onInterceptTouchEvent 方法, 判斷是否攔截事件
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // 沒有觸摸的目標, 而且不是初始化事件 ACTION_DOWN 了, 則直接將標記位設置爲攔截
            intercepted = true;
        }
        // 3. 不是 ACTION_CANCEL 事件, 而且沒有本身被攔截, 則開始尋找響應事件的子 View
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            // 只有 Down 事件纔會去找尋響應事件的子 View
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                    final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);
                    // 3.1 判斷這個子 View 是否能夠接收事件, 事件是否在它的區域內
                    if (!canViewReceivePointerEvents(child)
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                        ev.setTargetAccessibilityFocus(false);
                        continue;
                    }

                    //... 走到這裏說明在咱們手指觸摸的區域, 已經找到了一個子 View

                    // 3.2 判斷當前響應事件序列的鏈表中, 這個 View 是否已經在響應一個事件序列了
                    // Sample: 當你一根手指按在了這個子 View 上, 令一根手指也按在了這個子View上時, 不會從新響應 Down 事件的
                    newTouchTarget = getTouchTarget(child);
                    if (newTouchTarget != null) {
                        // 若當前找到的 View 正在響應事件, 則跳出循環
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                        break;
                    }
                    // 3.3 找到的 View 沒有在處理事件, 則經過 dispatchTransformedTouchEvent 將事件傳遞到這個子 View 中
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // ... 忽略部分代碼, 只關注細節
                        // 3.3.1 走到這裏, 說明事件成功分發給了子 View 而且被其成功消費了
                        // 3.3.2 調用 addTouchTarget 給 mFirstTouchTarget 賦值
                        // 3.3.3 若是處理掉了的話,將此 child 添加到 TouchTarget 鏈表的首部, 並讓 mFirstTouchTarget -> 鏈表的首部
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        break;
                    }
                }

                // 4. 本次沒找到響應該事件的 View, 但以前存在響應當前事件序列的 View (即當前 ViewGroup 的 mFirstTouchTarget 鏈不爲 null)
                // 例如我一開始一根手指按在了一個 Button 上, 後來我另外一根手指按在了 ViewGroup 空白的地方, 那麼就會走下面的邏輯
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // 遍歷鏈表
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    // while 結束後,newTouchTarget 指向了鏈表末尾的 TouchTarget 實例
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }

        }

        if (mFirstTouchTarget == null) {
            // 5. mFirstTouchTarget == null 則說明沒有任何子 View 消耗當前事件序列
            // 調用 dispatchTransformedTouchEvent 交由本身處理
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 6. 走到這裏說明存在一個子 View 正在響應事件序列
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            // 遍歷 mFirstTouchTarget 造成的鏈表中
            while (target != null) {
                final TouchTarget next = target.next;
                // 6.1 遍歷到的 target 剛好爲 newTouchTarget 直接標記爲 handle, 通常 ACTION_DOWN 會進入到這個 if 裏去, 由於 Down 事件的分發在上面進行
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 判斷是否須要向以前響應事件的子 view 發送 ACTION_CANCEL 事件
                    // 當前 ViewGroup 的父容器忽然攔截了事件序列, 咱們收到了 ACTION_CANCEL 事件
                    // 當前 ViewGroup 本身攔截了事件序列, 給子 View 分發 ACTION_CANCEL 事件
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    // 6.2 將事件繼續分發給子 View, 用於分發 ACTION_DOWN 之外的事件
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true; // TouchTarget 鏈中任意一個處理了則設置 handled 爲true
                    }
                    // 6.3 若是須要 cancelChild 的話,
                    // 則將此節點從 mFirstTouchTourget 所在的鏈表中回收到 TouchTarget.sRecycleBin 所在的鏈表中
                    if (cancelChild) {
                        if (predecessor == null) {
                            // 若表頭須要被回收, 則移動表頭到下一個位置
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target; // 訪問下一個節點
                target = next;
            }
        }
        return handle;
    }
    
    /**
     * ViewGroup.dispatchTransformedTouchEvent
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        // ... 只關注核心部分
        if (child == null) {
            // 若子 View 爲 null, 則回調當前 ViewGroup 父類的 dispatchTouchEvent 方法, 即本身處理
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            // 若子 View 不爲null, 則把事件傳遞給子 View 處理
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        transformedEvent.recycle();
        return handled;
    }
    
    /**
     * 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;
            // 1. 先執行 onTouchListener
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            // 2. onTouchListener 返回 false 會執行 onTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        return result;
    }
複製代碼

好了事件的分發到此出就結束了, 全部的重點仍是 ViewGroup 中的 dispatchTouchEvent 這個方法中, 尤爲是對 TouchTarget 鏈表的理解, 簡單的總結一下ui

ViewGroup.dispatchTouchEvent 幾個關鍵點:this

  1. 判斷是否須要攔截這個事件spa

    • 事件爲 ACTION_DOWN 會判斷是否須要攔截
    • 當前 ViewGroup 已經有子 View 消費事件序列了, 即 mFirstTarget != null 會判斷是否須要攔截
  2. 若爲 ACTION_DOWN 事件: 找尋可以響應這個事件的 View, 即給 newTouchTarget 賦值指針

    • 若找到子 View 且沒有處理其餘事件, 則調用 dispatchTransformedTouchEvent 分發給子 View 處理這個事件, 處理成功則給 newTouchTarget 賦值且綁定這個 View, 最後將 newTouchTarget 鏈入表頭, 更新表頭 mFirstTouchTaget 的值
    • 若沒找到子 View 或者子 View 正在處理其餘事件
      • mFirstTouchTarget != null : newTouchTarget 爲 mFirstTouchTarget 鏈表最後一個元素
      • mFirstTouchTarget == null: 調用 dispatchTransformedTouchEvent 本身處理
  3. 若爲 ACTION_DOWN 之外的其餘事件:rest

    • 事件被當前 ViewGroup 攔截: 給響應這個事件的子 View 分發 ACTION_CANCEL, 且將其對應的 TouchTarget 對象, 從鏈表中移除
    • 事件未被當前 ViewGroup 攔截: 調用 dispatchTransformedTouchEvent 正常分發

View.dispatchTouchEvent 中的關鍵點:
若是設置了 onTouchListener 而且返回了 true, 是不會回調 onTouchEvent 方法的code

具體的細節源碼中的註釋寫的很詳細, 就再也不贅述了orm

View 的 onTouchEvent 事件

public boolean onTouchEvent(MotionEvent event) { // View對touch事件的默認處理邏輯
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        // 判斷是否可響應點擊
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
         // DISABLED 的狀態下
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // 若是以前是PRESSED狀態則復原
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // Disable 的視圖依舊能夠響應觸摸事件, 只不過它是以無響應的方式
            // 只不過是以無響應的方式處理了
            return clickable;
        }
        // 若是有 TouchDelegate 的話,優先交給它處理
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) { 
                return true; // 處理了返回 true,不然接着往下走
            }
        }
        
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    // 若爲不可點擊的則移除相關回調, 直接結束對 ACTION_UP 的處理
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    // 若是外圍有能夠滾動的parent的話,當按下時會設置這個標誌位
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // 設置了FocusableInTouchMode後,View在點擊的時候就會
                        // 嘗試requestFocus(),並將focusToken設置爲true
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // 確保用戶可以看到按鈕的Press狀態
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { 
                            // 若是沒有長按發生的話, 移除長按 Callback
                            removeLongPressCallback();

                            // focusTaken 爲 false 才執行下面的對 onClick 的回調
                            // 若本次事件請求了焦點(focusTaken 爲 true), 則不會執行 onClick
                            if (!focusTaken) {
                                // 優先使用 post 去處理點擊事件, 這樣可讓視覺狀態在 performClick 以前呈現
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                 // 若是 post 失敗了,則直接調用 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();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }
                    // 判斷是否在能夠滾動的容器中
                    boolean isInScrollingContainer = isInScrollingContainer();
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED; // 設置 PREPRESSED 標誌位
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        // 延遲反饋
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // 不在能夠滾動的容器中, 則直接反饋
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    // 事件被父容器攔截了再也不下發, 則清除回調和標記位
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }
                    // 判斷是否移動到了當前 View 的界外
                    if (!pointInView(x, y, mTouchSlop)) {
                        // 移除回調
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }
            // 只要能進來就會返回 true
            return true;
        }

        return false;
    }
複製代碼

源碼註釋很詳細, 有幾個須要關注的點:對象

  1. 若不想讓子View消耗事件, 就必須
    • clickable 爲 false
    • (viewFlags & TOOLTIP) != TOOLTIP
// 判斷是否可響應點擊
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
複製代碼
  1. 若是當前 UP 事件執行了 requestFocus 則不會響應點擊事件
相關文章
相關標籤/搜索