<UI>View绘制及事件机制原理

一、View绘制流程机制

1、View绘制起点

  • performTraversals()方法触发了View 的绘制。

    Activity调用流程

  • 说明:
    在Activity显示时,WindowManager 将View添加到 DecorView ,两者通过 ViewRoot 连接起来。
    具体实现类是 ViewRootImpl
    再通过 ViewRootImpl 的一系列处理,最终调用 performTraversals 方法,在performTraversals 方法中,依次调用了 performMeasure()performLayout()performDraw(),将View 的measurelayout,draw` 过程从顶层View 分发了下去。开始了View的绘制。

2、View绘制流程

  • 绘制过程分为三步:
    measure(测量) --> layout(布局) --> draw(绘制),
    draw 流程结束以后就可以在屏幕上看到view了。
  • 流程图如下:


    绘制流程
.1、measure(测量)
  • 测量的目的,是为了计算出View的大小,通过MeasureSpec来进行计算的。
    MeasureSpec是一个 specSizespecMode 信息的32 位int 值,其中高两位表示 specMode,低30位表示 specSize

  • specMode 模式:
    UNSPECIFIED:父容器不对View有任何限制,要多大有多大。常用于系统内部。
    EXACTLY(精确模式):父视图为子视图指定一个确切的尺寸SpecSize。对应LyaoutParams中的match_parent或具体数值。
    AT_MOST(最大[限制]模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content。

  • 决定View大小的因素:
    因素:widthMeasureSpecheightMeasureSpec
    MeasureSpec 值由 子View的布局参数LayoutParams父容器的MeasureSpec值 共同决定。具体规则见下图:

    MeasureSpec创建规则

顶级View(即DecorView)的测量在ViewRootImpl的源码里(getRootMeasureSpec方法):

//desire的这2个参数就代表屏幕的宽高,
  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() 会调用 measure()(final方法),将计算的MeasureSpec 传递给调用的onMeasure()方法,此方法会调用setMeasuredDimension()来设置自身大小;
    如果是ViewGroup,先遍历子View,测量出子View的MeasureSpec ,再调用measureChild*相关方法让子View通过调用measure方法来测量自己的大小;再根据所有子View的大小确定自身的大小,其中的子View会重复此类的measure过程,如此反复至完成整个View树的遍历,确定各个View自身的大小。

注意:

  • ViewGroup中没有onMeasure方法
  • View会进行多次的测量,第一次测量和最终测量的实际宽高不一定相等,在layout流程中可确定View的实际宽高
  • 获取measure()后的宽高方法:
  • Activity#onWindowFocusChange()中获取
  • view.post(Runnable)将获取的代码投递到消息队列的尾部
  • ViewTreeObservable方法
.2、layout(布局)
  • 布局目的:确定View的 最终宽高四个顶点的位置

  • 流程:
    performLayout() 会调用顶级View的layout() 方法,其中调用 setFrame() 方法来设置其四个顶点(mLeft、mRight、mTop、mBottom);
    接着调用 onLayout() (空方法),此方法由具体实现的View自身重写,用来确定自身位置,及循环其子View来确定坐标位置,子View 会循环调用setChildFrame()(就是调用 View.layout())。

    layout流程

  • layout和onLayout方法有什么区别?
    layout是确定本身view的位置,通过serFrame方法设定本身view的四个顶点的位置。
    onLayout是确定所有子元素的位置。
    View和ViewGroup的 onLayout 方法都是空方法。都留给我们自己给子元素布局。

.3、draw(绘制)
  • 绘制目的:显示View
  • 流程:
    performMeasure() 会调用 ViewRootImpl的draw方法,再调用drawSoftWare()方法,其中会调用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自身的内容 —— onDraw(canvas)
    1. Draw children:绘制子View(分发) —— 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)调用了其drawChild方法

  • 绘制流程图:


    draw流程
  • 说明:

  • setWillNotDraw:用于设置绘制的标志位,view是否需要draw绘制。
    若自定义的View 不需要draw,则可以设置这个方法为true。系统会根据此标记来优化执行速度。
    ViewGroup 一般都默认设置这个为true,因为ViewGroup多数都是只负责布局,不负责draw的。
    View 的这个标志位默认一般都是关闭的。
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
  • 一般执行动画,会多次调用onDraw方法,通过监听动画的参数值变化,不断 invalidate,不断重绘。
    invalidate 是在 主线程 中进行调用,会引发onDraw进行重绘
    postInvalidate 是在 子线程 中调用,最终调用的仍是invalidate

参考链接:
Android View绘制13问13答
要点提炼|开发艺术之View

二、View及ViewGroup的事件机制

1、相关概念:

  • MotionEvent事件
  • 事件类型:
    ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
    ACTION_MOVE:手指在屏幕上移动时候产生该事件
    ACTION_UP:手指从屏幕上松开的瞬间产生该事件
  • 事件序列:
    从ACTION_DOWN开始到ACTION_UP结束我们称为一个事件序列
  • TouchSlop
    系统所能识别的被认为是滑动的最小距离。
    即当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。
    该常量和设备有关,可用它来判断用户的滑动是否达到阈值,获取方法:
   ViewConfiguration.get(getContext()).getScaledTouchSlop()
  • VelocityTracker
    速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。

  • GestureDetector
    手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。

2、事件分发的要点

  • 事件分发的本质:
    是对MotionEvent事件分发的过程,并将事件消费处理。

  • 事件分发的传递顺序:
    Activity(Window) --> ViewGroup --> View
    即最终调用的是 View的dispatchTouchEvent(MotionEvent event)方法

  • 事件分发的重要方法:
    1、dispatchTouchEvent(MotionEvent event):分发事件,返回boolean类型,表示事件是否消费
    2、onInterceptTouchEvent(MotionEvent event):中断事件(仅ViewGroup有),返回boolean类型,表示事件是否中断
    3、onTouchEvent(MotionEvent event):消费事件(仅View有),返回boolean类型,表示事件是否消费
    4、perform*Click():执行事件(仅View有),最终是onClick(View view),或长按、双击等事件处理。

  • 事件分发伪代码:

   // ViewGroup
   public boolean dispatchTouchEvent(MotionEvent ev) {
       // 事件是否被消费
       boolean consume = false;
       // 调用onInterceptTouchEvent判断是否拦截事件
       if (onInterceptTouchEvent(ev)) {
           // 如果拦截则调用自身的onTouchEvent方法
           consume = onTouchEvent(ev);
       } else {
           if (targetChild == null) {
              // 没有找到目标child,则调用父容器的分发方法
              consume = super.dispatchTouchEvent(ev);
           } else {
              // 不拦截调用子View的dispatchTouchEvent方法
              consume = child.dispatchTouchEvent(ev);
           }
       }
       // 返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
       return consume;
   }

   // View
   public boolean dispatchTouchEvent(MotionEvent ev) {
       boolean consume = false;
       // 是否实现了TouchListener#onTouch方法
       if (onTouchListener != null) {
           // 调用实现的onTouchListener#onTouch方法
           consume = onTouchListener.onTouch(ev);
       } else {
           // onTouchEvent()中调用了perform*Click()等方法
           consume = onTouchEvent(ev);
       }
       //返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
       return consume;
   }

3、事件分发的机制&流程

  • 示意图


    View及ViewGroup事件流程示意图
流程详述:
  • 1、ViewGroup分发开端:
    Acivity#dispatchTouchEvent(MotionEvent)
    事件最开始从Activity开始,由Acivity的dispatchTouchEvent方法来对事件进行分发。
    // Activity源码:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 事件分发并返回结果
        if (getWindow().superDispatchTouchEvent(ev)) {
            //事件被消费
            return true;
        }
        // 无View 消费事件,则调用Activity#onTouchEvent方法
        return onTouchEvent(ev);
    }

PhoneWindow#superDispatchTouchEvent(MotionEvent)
getWindow().superDispatchTouchEvent(ev)Window的抽象方法,具体由PhoneWindow实现
其内部是调用的顶级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,其方法中调用了ViewGroup#dispatchTouchEvent方法

    public boolean superDispatchTouchEvent(MotionEvent event) {
        // 此处调用的是ViewGroup的dispatchTouchEvent方法
        return super.dispatchTouchEvent(event);
    }
  • 2、ViewGroup事件分发 — onInterceptTouchEvent
    MotionEvent.ACTION_DOWN事件:
    先判断是否为MotionEvent.ACTION_DOWN事件,是,则清除 FLAG_DISALLOW_INTERCEPT 设置并且mFirstTouchTarget 设置为null,然后根据条件调用 onInterceptTouchEvent方法,来处理拦截事件。
    如果不是MotionEvent.ACTION_DOWN事件,且mFirstTouchTarget == null,则直接设置中断标记为true(intercepted = true),ViewGroup直接拦截其他事件(如MOVE和UP等)进行处理。
    FLAG_DISALLOW_INTERCEPT标志位:
    如果通过requestDisallowInterceptTouchEvent方法设置了此标志位,则子View可以以此来干预父View的事件分发过程(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) {
                //调用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事件分发 — 子View遍历mFirstTouchTarget
    当ViewGroup不拦截事件,则遍历子View,找到事件接收的目标View(mFirstTouchTarget),条件:

    • View可见且没有播放动画:canViewReceivePointerEvents方法
    • 事件的坐标落在View的范围内:isTransformedTouchPointInView

    当mFirstTouchTarget不为null,则说明已经找到过了目标child,则newTouchTarget不为null,会跳出循环。
    但此时还没有将事件分发给子View,所以newTouchTarget为null,mFirstTouchTarget也是null。
    如果dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法返回了true,即子View消费了事件,则会将mFirstTouchTarget进行赋值为该子View,终止子View的遍历。此时,子View的遍历完成。
    dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法中,如果子View不为null,则调用了子View的child.dispatchTouchEvent分发方法,进行View的分发。

    // 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的范围内
            //如果上述两个条件有一项不满足则continue继续循环下一个View
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            newTouchTarget = getTouchTarget(child);
            //如果有子View处理即newTouchTarget 不为null则跳出循环。
            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第三个参数child这里不为null
            //实际调用的是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循环
                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事件分发 — View#dispatchTouchEvent
    若遍历子View后,ViewGroup没有找到事件处理者(① ViewGroup没有子View 或 ② 子View处理了事件却在dispatchTouchEvent方法返回了false),则ViewGroup会去处理这个事件。
    dispatchTouchEvent方法返回了false,mFirstTouchTarget 必然为null,则再次调用自身的dispatchTransformedTouchEvent 方法(但传入的child为null),其内部会调用super.dispatchTouchEvent(event);方法,将调用View#dispatchTouchEvent,将事件传给View,至此,ViewGroup的分发过程完成。
   // 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的事件分发View#dispatchTouchEvent
    mOnTouchListener.onTouch监听:
    View中首先判断是否设置了OnTouchListener监听(开发者自己实现的),若设置了且onTouch返回true,则之后的onTouchEvent方法不会调用;若没有设置监听或 onTouch 返回 false,则会调用onTouchEvent方法。
    onTouchEvent方法:
    在此方法中,具体的处理了各个事件。
    如果View设置成了disabled状态(即不可用),只要 CLICKABLELONG_CLICKABLE 有一个为true,就一定会消费这个事件(即onTouchEvent返回true),只是它看起来不可用。只有不可点击(clickablelongClickable同时为false),才会返回false,即onTouchEvent不消费此事件。
    performClick()方法:
    就点击事件而言,在ACTION_UP 事件的条件下,会调用performClickInternal方法(内部实际是performClick());在performClick()方法中,如果设置了OnClickListener,则会回调onClick方法。

dispatchTouchEvent(MotionEvent)

     // View源码:
    //如果窗口没有被遮盖
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        //当前监听事件
        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调用自己的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)) {
                                    // 其内部调用的是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、事件分发的相关问题:

-- 问题:如果一个事件序列的 ACTION_DOWN 事件被 ViewGroup 拦截,此时子 View 调用 requestDisallowInterceptTouchEvent 方法有没有用?

子View可以通过 requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外),而这就是我们处理滑动冲突常用的关键方法。

requestDisallowInterceptTouchEvent 中,设置了 FLAG_DISALLOW_INTERCEPT 标志位,表示子View不希望此父级及其祖先使用 ViewGroup.onInterceptTouchEvent(MotionEvent) 拦截触摸事件。

ACTION_DOWN 事件是清除了这个标志位的,所以,requestDisallowInterceptTouchEvent 的设置对ACTION_DOWN无效。

    // ViewGroup源码,实现的是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) {
                //调用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进行事件分发了。则会取消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 去分发,最终调用的是自身的 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)) {
                   //内部会比较 pointerIdBits 和当前事件的 pointerIdBits,一致才会处理
                   //这便是 Down 事件处理后后续事件都交给该 View 处理的原理
                   handled = true;
               }
               if (cancelChild) {
                   if (predecessor == null) {
                       mFirstTouchTarget = next;
                   } else {
                       predecessor.next = next;
                   }
                   target.recycle();
                   // 没有next则为null,就结束了循环
                   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事件分发机制
必问的事件分发,你答得上来吗
Android事件分发机制详解:史上最全面、最易懂
要点提炼|开发艺术之View

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容