<UI>View繪制及事件機制原理

一、View繪制流程機制

1、View繪制起點

  • performTraversals()方法觸發(fā)了View 的繪制。

    Activity調(diào)用流程

  • 說明:
    在Activity顯示時,WindowManager 將View添加到 DecorView ,兩者通過 ViewRoot 連接起來。
    具體實現(xiàn)類是 ViewRootImpl
    再通過 ViewRootImpl 的一系列處理,最終調(diào)用 performTraversals 方法,在performTraversals 方法中,依次調(diào)用了 performMeasure(),performLayout(),performDraw(),將View 的measurelayout,draw` 過程從頂層View 分發(fā)了下去。開始了View的繪制。

2、View繪制流程

  • 繪制過程分為三步:
    measure(測量) --> layout(布局) --> draw(繪制),
    draw 流程結束以后就可以在屏幕上看到view了。
  • 流程圖如下:


    繪制流程
.1、measure(測量)
  • 測量的目的,是為了計算出View的大小,通過MeasureSpec來進行計算的。
    MeasureSpec是一個 specSizespecMode 信息的32 位int 值,其中高兩位表示 specMode,低30位表示 specSize

  • specMode 模式:
    UNSPECIFIED:父容器不對View有任何限制,要多大有多大。常用于系統(tǒng)內(nèi)部。
    EXACTLY(精確模式):父視圖為子視圖指定一個確切的尺寸SpecSize。對應LyaoutParams中的match_parent或具體數(shù)值。
    AT_MOST(最大[限制]模式):父容器為子視圖指定一個最大尺寸SpecSize,View的大小不能大于這個值。對應LayoutParams中的wrap_content。

  • 決定View大小的因素:
    因素:widthMeasureSpecheightMeasureSpec
    MeasureSpec 值由 子View的布局參數(shù)LayoutParams父容器的MeasureSpec值 共同決定。具體規(guī)則見下圖:

    MeasureSpec創(chuàng)建規(guī)則

頂級View(即DecorView)的測量在ViewRootImpl的源碼里(getRootMeasureSpec方法):

//desire的這2個參數(shù)就代表屏幕的寬高,
  childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
  childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

  //decorView的measureSpec就是在這里確定的,其實比普通view的measurespec要簡單的多
  private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
}
  • 流程:
    performMeasure() 會調(diào)用 measure()(final方法),將計算的MeasureSpec 傳遞給調(diào)用的onMeasure()方法,此方法會調(diào)用setMeasuredDimension()來設置自身大??;
    如果是ViewGroup,先遍歷子View,測量出子View的MeasureSpec ,再調(diào)用measureChild*相關方法讓子View通過調(diào)用measure方法來測量自己的大??;再根據(jù)所有子View的大小確定自身的大小,其中的子View會重復此類的measure過程,如此反復至完成整個View樹的遍歷,確定各個View自身的大小。

注意:

  • ViewGroup中沒有onMeasure方法
  • View會進行多次的測量,第一次測量和最終測量的實際寬高不一定相等,在layout流程中可確定View的實際寬高
  • 獲取measure()后的寬高方法:
  • Activity#onWindowFocusChange()中獲取
  • view.post(Runnable)將獲取的代碼投遞到消息隊列的尾部
  • ViewTreeObservable方法
.2、layout(布局)
  • 布局目的:確定View的 最終寬高四個頂點的位置

  • 流程:
    performLayout() 會調(diào)用頂級View的layout() 方法,其中調(diào)用 setFrame() 方法來設置其四個頂點(mLeft、mRight、mTop、mBottom);
    接著調(diào)用 onLayout() (空方法),此方法由具體實現(xiàn)的View自身重寫,用來確定自身位置,及循環(huán)其子View來確定坐標位置,子View?會循環(huán)調(diào)用setChildFrame()(就是調(diào)用 View.layout())。

    layout流程

  • layout和onLayout方法有什么區(qū)別?
    layout是確定本身view的位置,通過serFrame方法設定本身view的四個頂點的位置。
    onLayout是確定所有子元素的位置。
    View和ViewGroup的 onLayout 方法都是空方法。都留給我們自己給子元素布局。

.3、draw(繪制)
  • 繪制目的:顯示View
  • 流程:
    performMeasure() 會調(diào)用 ViewRootImpl的draw方法,再調(diào)用drawSoftWare()方法,其中會調(diào)用mView.draw(),是真正繪制步驟的開始。繪制步驟如下:(1、3、4、6四步為主要步驟)
  • 1、Draw the background:繪制背景 —— drawBackground(canvas)
    1. If necessary, save the canvas' layers to prepare for fading:保存圖層
    1. Draw view's content:繪制view自身的內(nèi)容 —— onDraw(canvas)
    1. Draw children:繪制子View(分發(fā)) —— dispatchDraw(canvas)
    1. If necessary, draw the fading edges and restore layers:繪制一些圖層
    1. Draw decorations (scrollbars for instance):繪制裝飾(如滾動條) —— onDrawForeground(canvas)

注:View中的dispatchDraw(canvas) 是空方法,ViewGroup中的dispatchDraw(canvas)調(diào)用了其drawChild方法

  • 繪制流程圖:


    draw流程
  • 說明:

  • setWillNotDraw:用于設置繪制的標志位,view是否需要draw繪制。
    若自定義的View 不需要draw,則可以設置這個方法為true。系統(tǒng)會根據(jù)此標記來優(yōu)化執(zhí)行速度。
    ViewGroup 一般都默認設置這個為true,因為ViewGroup多數(shù)都是只負責布局,不負責draw的。
    View 的這個標志位默認一般都是關閉的。
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
  • 一般執(zhí)行動畫,會多次調(diào)用onDraw方法,通過監(jiān)聽動畫的參數(shù)值變化,不斷 invalidate,不斷重繪。
    invalidate 是在 主線程 中進行調(diào)用,會引發(fā)onDraw進行重繪
    postInvalidate 是在 子線程 中調(diào)用,最終調(diào)用的仍是invalidate

參考鏈接:
Android View繪制13問13答
要點提煉|開發(fā)藝術之View

二、View及ViewGroup的事件機制

1、相關概念:

  • MotionEvent事件
  • 事件類型:
    ACTION_DOWN:手指剛接觸屏幕,按下去的那一瞬間產(chǎn)生該事件
    ACTION_MOVE:手指在屏幕上移動時候產(chǎn)生該事件
    ACTION_UP:手指從屏幕上松開的瞬間產(chǎn)生該事件
  • 事件序列:
    從ACTION_DOWN開始到ACTION_UP結束我們稱為一個事件序列
  • TouchSlop
    系統(tǒng)所能識別的被認為是滑動的最小距離。
    即當手指在屏幕上滑動時,如果兩次滑動之間的距離小于這個常量,那么系統(tǒng)就不認為你是在進行滑動操作。
    該常量和設備有關,可用它來判斷用戶的滑動是否達到閾值,獲取方法:
   ViewConfiguration.get(getContext()).getScaledTouchSlop()
  • VelocityTracker
    速度追蹤,用于追蹤手指在滑動過程中的速度,包括水平和豎直方向的速度。

  • GestureDetector
    手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。

2、事件分發(fā)的要點

  • 事件分發(fā)的本質:
    是對MotionEvent事件分發(fā)的過程,并將事件消費處理。

  • 事件分發(fā)的傳遞順序:
    Activity(Window) --> ViewGroup --> View
    即最終調(diào)用的是 View的dispatchTouchEvent(MotionEvent event)方法

  • 事件分發(fā)的重要方法:
    1、dispatchTouchEvent(MotionEvent event):分發(fā)事件,返回boolean類型,表示事件是否消費
    2、onInterceptTouchEvent(MotionEvent event):中斷事件(僅ViewGroup有),返回boolean類型,表示事件是否中斷
    3、onTouchEvent(MotionEvent event):消費事件(僅View有),返回boolean類型,表示事件是否消費
    4、perform*Click():執(zhí)行事件(僅View有),最終是onClick(View view),或長按、雙擊等事件處理。

  • 事件分發(fā)偽代碼:

   // ViewGroup
   public boolean dispatchTouchEvent(MotionEvent ev) {
       // 事件是否被消費
       boolean consume = false;
       // 調(diào)用onInterceptTouchEvent判斷是否攔截事件
       if (onInterceptTouchEvent(ev)) {
           // 如果攔截則調(diào)用自身的onTouchEvent方法
           consume = onTouchEvent(ev);
       } else {
           if (targetChild == null) {
              // 沒有找到目標child,則調(diào)用父容器的分發(fā)方法
              consume = super.dispatchTouchEvent(ev);
           } else {
              // 不攔截調(diào)用子View的dispatchTouchEvent方法
              consume = child.dispatchTouchEvent(ev);
           }
       }
       // 返回值表示事件是否被消費,true事件終止,false調(diào)用父View的onTouchEvent方法
       return consume;
   }

   // View
   public boolean dispatchTouchEvent(MotionEvent ev) {
       boolean consume = false;
       // 是否實現(xiàn)了TouchListener#onTouch方法
       if (onTouchListener != null) {
           // 調(diào)用實現(xiàn)的onTouchListener#onTouch方法
           consume = onTouchListener.onTouch(ev);
       } else {
           // onTouchEvent()中調(diào)用了perform*Click()等方法
           consume = onTouchEvent(ev);
       }
       //返回值表示事件是否被消費,true事件終止,false調(diào)用父View的onTouchEvent方法
       return consume;
   }

3、事件分發(fā)的機制&流程

  • 示意圖


    View及ViewGroup事件流程示意圖
流程詳述:
  • 1、ViewGroup分發(fā)開端:
    Acivity#dispatchTouchEvent(MotionEvent)
    事件最開始從Activity開始,由Acivity的dispatchTouchEvent方法來對事件進行分發(fā)。
    // Activity源碼:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 事件分發(fā)并返回結果
        if (getWindow().superDispatchTouchEvent(ev)) {
            //事件被消費
            return true;
        }
        // 無View 消費事件,則調(diào)用Activity#onTouchEvent方法
        return onTouchEvent(ev);
    }

PhoneWindow#superDispatchTouchEvent(MotionEvent)
getWindow().superDispatchTouchEvent(ev)Window的抽象方法,具體由PhoneWindow實現(xiàn)
其內(nèi)部是調(diào)用的頂級View(DecorView)的superDispatchTouchEvent方法

    // PhoneWindow源碼:
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView.superDispatchTouchEvent(MotionEvent)
頂級View(DecorView)一般為ViewGroup,其方法中調(diào)用了ViewGroup#dispatchTouchEvent方法

    public boolean superDispatchTouchEvent(MotionEvent event) {
        // 此處調(diào)用的是ViewGroup的dispatchTouchEvent方法
        return super.dispatchTouchEvent(event);
    }
  • 2、ViewGroup事件分發(fā) — onInterceptTouchEvent
    MotionEvent.ACTION_DOWN事件:
    先判斷是否為MotionEvent.ACTION_DOWN事件,是,則清除 FLAG_DISALLOW_INTERCEPT 設置并且mFirstTouchTarget 設置為null,然后根據(jù)條件調(diào)用 onInterceptTouchEvent方法,來處理攔截事件。
    如果不是MotionEvent.ACTION_DOWN事件,且mFirstTouchTarget == null,則直接設置中斷標記為true(intercepted = true),ViewGroup直接攔截其他事件(如MOVE和UP等)進行處理。
    FLAG_DISALLOW_INTERCEPT標志位:
    如果通過requestDisallowInterceptTouchEvent方法設置了此標志位,則子View可以以此來干預父View的事件分發(fā)過程(ACTION_DOWN事件除外,上面的原因),而這就是我們處理滑動沖突常用的關鍵方法。
    // ViewGroup源碼:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        final int action = ev.getAction();
        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.
            cancelAndClearTouchTargets(ev);
            //清除FLAG_DISALLOW_INTERCEPT設置并且mFirstTouchTarget 設置為null
            resetTouchState();
        }
        // Check for interception.
        final boolean intercepted;//是否攔截事件
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT是子View通過
            //requestDisallowInterceptTouchEvent方法進行設置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //調(diào)用onInterceptTouchEvent方法判斷是否需要攔截
                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;
        }
        ...
    }
  • 3、ViewGroup事件分發(fā) — 子View遍歷mFirstTouchTarget
    當ViewGroup不攔截事件,則遍歷子View,找到事件接收的目標View(mFirstTouchTarget),條件:

    • View可見且沒有播放動畫:canViewReceivePointerEvents方法
    • 事件的坐標落在View的范圍內(nèi):isTransformedTouchPointInView

    當mFirstTouchTarget不為null,則說明已經(jīng)找到過了目標child,則newTouchTarget不為null,會跳出循環(huán)。
    但此時還沒有將事件分發(fā)給子View,所以newTouchTarget為null,mFirstTouchTarget也是null。
    如果dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法返回了true,即子View消費了事件,則會將mFirstTouchTarget進行賦值為該子View,終止子View的遍歷。此時,子View的遍歷完成。
    dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法中,如果子View不為null,則調(diào)用了子View的child.dispatchTouchEvent分發(fā)方法,進行View的分發(fā)。

    // ViewGroup源碼:
    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);

            // 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;
            }

            //判斷1,View可見并且沒有播放動畫。2,點擊事件的坐標落在View的范圍內(nèi)
            //如果上述兩個條件有一項不滿足則continue繼續(xù)循環(huán)下一個View
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            newTouchTarget = getTouchTarget(child);
            //如果有子View處理即newTouchTarget 不為null則跳出循環(huán)。
            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);
            //dispatchTransformedTouchEvent第三個參數(shù)child這里不為null
            //實際調(diào)用的是child的dispatchTouchEvent方法
            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();
                //當child處理了點擊事件,那么會設置mFirstTouchTarget 在addTouchTarget被賦值
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                //子View處理了事件,然后就跳出了for循環(huán)
                break;
            }
        }
    }

dispatchTransformedTouchEvent方法:

    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }

mFirstTouchTarget的賦值:

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
  • 4、ViewGroup事件分發(fā) — View#dispatchTouchEvent
    若遍歷子View后,ViewGroup沒有找到事件處理者(① ViewGroup沒有子View 或 ② 子View處理了事件卻在dispatchTouchEvent方法返回了false),則ViewGroup會去處理這個事件。
    dispatchTouchEvent方法返回了false,mFirstTouchTarget 必然為null,則再次調(diào)用自身的dispatchTransformedTouchEvent 方法(但傳入的child為null),其內(nèi)部會調(diào)用super.dispatchTouchEvent(event);方法,將調(diào)用View#dispatchTouchEvent,將事件傳給View,至此,ViewGroup的分發(fā)過程完成。
   // 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);
   }
  • 5、View的事件分發(fā)View#dispatchTouchEvent
    mOnTouchListener.onTouch監(jiān)聽:
    View中首先判斷是否設置了OnTouchListener監(jiān)聽(開發(fā)者自己實現(xiàn)的),若設置了且onTouch返回true,則之后的onTouchEvent方法不會調(diào)用;若沒有設置監(jiān)聽或 onTouch 返回 false,則會調(diào)用onTouchEvent方法。
    onTouchEvent方法:
    在此方法中,具體的處理了各個事件。
    如果View設置成了disabled狀態(tài)(即不可用),只要 CLICKABLELONG_CLICKABLE 有一個為true,就一定會消費這個事件(即onTouchEvent返回true),只是它看起來不可用。只有不可點擊(clickablelongClickable同時為false),才會返回false,即onTouchEvent不消費此事件。
    performClick()方法:
    就點擊事件而言,在ACTION_UP 事件的條件下,會調(diào)用performClickInternal方法(內(nèi)部實際是performClick());在performClick()方法中,如果設置了OnClickListener,則會回調(diào)onClick方法。

dispatchTouchEvent(MotionEvent)

     // View源碼:
    //如果窗口沒有被遮蓋
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        //當前監(jiān)聽事件
        ListenerInfo li = mListenerInfo;
        //需要特別注意這個判斷當中的li.mOnTouchListener.onTouch(this, event)條件
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //result為false調(diào)用自己的onTouchEvent方法處理
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

onTouchEvent(MotionEvent)

// View源碼:
public boolean onTouchEvent(MotionEvent event) {
        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;

        // 判斷是否不可用,但仍會消費事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // 此處重點
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                                if (!post(mPerformClick)) {
                                    // 其內(nèi)部調(diào)用的是performClick方法
                                    performClickInternal();
                                }
                    ...
                    break;
                    ...
            }

            return true;
        }

        return false;
    }

performClick()

   // View源碼:
    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    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;
    }

說明: setClickable 失效的原因:
View的setOnClickListener會默認將View的clickable設置成true。
View的setOnLongClickListener同樣會將View的longClickable設置成true。

    // View源碼:
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

4、事件分發(fā)的相關問題:

-- 問題:如果一個事件序列的 ACTION_DOWN 事件被 ViewGroup 攔截,此時子 View 調(diào)用 requestDisallowInterceptTouchEvent 方法有沒有用?

子View可以通過 requestDisallowInterceptTouchEvent方法干預父View的事件分發(fā)過程(ACTION_DOWN事件除外),而這就是我們處理滑動沖突常用的關鍵方法。

requestDisallowInterceptTouchEvent 中,設置了 FLAG_DISALLOW_INTERCEPT 標志位,表示子View不希望此父級及其祖先使用 ViewGroup.onInterceptTouchEvent(MotionEvent) 攔截觸摸事件。

ACTION_DOWN 事件是清除了這個標志位的,所以,requestDisallowInterceptTouchEvent 的設置對ACTION_DOWN無效。

    // ViewGroup源碼,實現(xiàn)的是ViewParent的抽象方法
    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

    // ViewGroup#dispatchTouchEvent
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            ....
            //清除FLAG_DISALLOW_INTERCEPT設置并且mFirstTouchTarget 設置為null
            resetTouchState();
        }
        //是否攔截事件
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT是子View通過
            //requestDisallowInterceptTouchEvent方法進行設置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //調(diào)用onInterceptTouchEvent方法判斷是否需要攔截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        ...
    }

-- 問題:ACTION_DOWN 事件被子 View 消費了,那 ViewGroup 能攔截剩下的事件嗎?如果攔截了剩下事件,當前這個事件 ViewGroup 能消費嗎?子 View 還會收到事件嗎?

ACTION_DOWN 事件被子 View 消費后,mFirstTouchTarget 則不為null了,就會直接攔截其他事件,intercepted = true;,見下面的源碼。
設置了攔截,就不會再遍歷子View進行事件分發(fā)了。則會取消cancelChild,攔截子View的事件。

   // ViewGroup#dispatchTouchEvent
   if (actionMasked == MotionEvent.ACTION_DOWN
           || mFirstTouchTarget != null) {
       // onInterceptTouchEvent方法的判斷
       ......
   } else {
       // 重點在這里
       // There are no touch targets and this action is not an initial down
       // so this view group continues to intercept touches.
       intercepted = true;    // <---------------------------------------重點
   }

   ....
   // Dispatch to touch targets.
   if (mFirstTouchTarget == null) {
       // 沒有子 View 消費事件,則傳入 null 去分發(fā),最終調(diào)用的是自身的 onTouchEvent 方法,進行處理 touch 事件
       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;
           if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
               handled = true;
           } else {
               // 如果 intercepted 就取消 cancelChild,這便是攔截子 View 事件的原理
               final boolean cancelChild = resetCancelNextUpFlag(target.child)
                       || intercepted;   // <---------------------------------------重點
               if (dispatchTransformedTouchEvent(ev, cancelChild,
                       target.child, target.pointerIdBits)) {
                   //內(nèi)部會比較 pointerIdBits 和當前事件的 pointerIdBits,一致才會處理
                   //這便是 Down 事件處理后后續(xù)事件都交給該 View 處理的原理
                   handled = true;
               }
               if (cancelChild) {
                   if (predecessor == null) {
                       mFirstTouchTarget = next;
                   } else {
                       predecessor.next = next;
                   }
                   target.recycle();
                   // 沒有next則為null,就結束了循環(huán)
                   target = next;
                   continue;
               }
           }
           predecessor = target;
           target = next;
       }
   }
-- 問題:當 View Disable 時,會消費事件嗎?

會消費事件,只是設為了不可用,可以看到,在源碼中的注釋為:

A disabled view that is clickable still consumes the touch events, it just doesn't respond to them.
[一個可點擊的禁用view,仍然可以消費事件,它只是沒有響應它們(事件)而已。]

    // View#onTouchEvent
   // 判斷是否不可用,但仍會消費事件
   if ((viewFlags & ENABLED_MASK) == DISABLED) {
       if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
           setPressed(false);
       }
       mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
       // 此處重點
       // A disabled view that is clickable still consumes the touch
       // events, it just doesn't respond to them.
       return clickable;
   }

參考鏈接:
一文讀懂Android View事件分發(fā)機制
必問的事件分發(fā),你答得上來嗎
Android事件分發(fā)機制詳解:史上最全面、最易懂
要點提煉|開發(fā)藝術之View

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容