android学习

Android面试Android进阶(十六)-事件分发相关

2021-04-18  本文已影响0人  肖义熙

问:描述一下Android事件分发流程

答:Android事件指的是:MotionEvent的四种状态(ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL),Android的事件是从Activity开始的,中间经过Window及DecorView,其中Window的唯一实现类为PhoneWindow,DecorView是一个FrameLayout(其中有一个LinearLayout,id为content)的顶级View。之后通过dispatchTouchEvent方法分发到ViewGroup、View。即整个事件分发的流程是:
Activity -》PhoneWindow -》DecorView -》ViewGroup -》View
各个层级都可以对其进行消费,一旦消费则不再往下分发。
如果事件分发过程中都没有进行处理,则会进行反方向回传,最终回传给Activity,如果Activity也没有处理,则抛弃本次事件
View -》ViewGroup -》DevorView -》PhoneWindow -》Activity
这里其实是一个设计模式的使用:责任链模式,有想了解的可以去了解一下。
PhoneWindow、DecorView这两个步骤在我们平时及自定义View时不需要进行处理,这里简单说一下Activity、ViewGroup、View的事件分发。

类型 方法 Activity ViewGroup View
事件分发 dispatchTouchEvent true true true
事件拦截 onInterceptTouchEvent false true false
事件消费 onTouchEvent true true true

从上表可以看出,Activity和View是没有事件拦截的,因为:
1、Activity作为事件开始的地方,原始事件分发者如果拦截了,将导致整个屏幕都无法响应事件。
2、View作为事件传递的最终端,要么消费事件、要么回传事件,没有必要进行拦截。

举几个形象生动的例子来说明dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent的关系,首先先把角色定义一下。就拿公司组织架构来讲:
Activity:老板

//MainActivity,重写dispatchTouchEvent、onTouchEvent方法
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.e("XYX", "MainActivity-dispatchTouchEvent 老板:经理,我看这个电商行业现在很火啊,看着模仿一个淘宝出来吧,下周能做好吗?")
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.e("XYX", "MainActivity-onTouchEvent 老板:都干啥吃的,这么简单的东西都做不了")
        return super.onTouchEvent(event)
    }
}

ViewGroupA:项目经理

//ViewGroupA:继承自LinearLayout,是一个ViewGroup,重写分发、拦截、消费事件方法:
class ViewGroupA : LinearLayout {
    /**
     * 在xml布局文件中使用时自动调用
     */
    constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {

    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupA-dispatchTouchEvent   项目经理:呼叫技术部,老板要做一个淘宝,你们评估一下,下周能不能上线")
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupA-onInterceptTouchEvent   项目经理:老板可能疯了,但是又不是我做,问问下面的人")
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupA-onTouchEvent   项目经理:报告老板,技术部说做不了(得把锅甩出去)")
        return super.onTouchEvent(event)
    }
}

ViewGroupB:技术总监

//ViewGroupB:技术总监  继承自LinearLayout,是一个ViewGroup,重写分发、拦截、消费事件方法:
class ViewGroupB : LinearLayout {

    /**
     * 在xml布局文件中使用时自动调用
     */
    constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {

    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupB-dispatchTouchEvent   技术总监:做一个淘宝?下周要上线?疯了吧!")
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupB-onInterceptTouchEvent   技术总监:肯定做不了,给小王传达一下老板的意思吧")
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupB-onTouchEvent   技术总监:小王说做不了(锅得甩出去,不是我做不了)")
        return super.onTouchEvent(event)
    }
}

View1:小王(就是你这个Android开发了)

//ViewA 小王  继承自View
class ViewA : View {

    /**
     * 在xml布局文件中使用时自动调用
     */
    constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {

    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.e("XYX", "ViewA-dispatchTouchEvent   小王:心里一万只草泥马飘过,老板疯了")
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.e("XYX", "ViewA-onTouchEvent   小王:虽然一万只草泥马,但是也要给回应啊,毕竟都是打工人:老大,这个我真心做不了啊")
        return super.onTouchEvent(event)
    }

}
显示效果: image.png

测试一下没有任何拦截与消费的情况下,全部默认实现时,点击ViewA后打印结果如下:


image.png

上面看见的老板提出这个需求以后,所有人都不发表意见,就把问题需求分发下去了,任何人都处理不了,交回给老板处理去了,老板只能大骂一声:都干啥吃的,这么简单都做不了!现在改一下,老板感觉小王能力太差,把他开了,招了一个Android大牛小李回来,新来的想要表现一下,什么活都能干,这不他就接下来一周做一个淘宝了:

    //修改ViewA中onTouchEvent方法:
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.e("XYX", "ViewA-onTouchEvent   小李:好的,交给我吧,一周做不完提头来见!")
//        return super.onTouchEvent(event)
        return true
    }

打印结果:


image.png

一周过去了,小李没有提头来见,而是被老板开了(什么原因大家心里清楚),又新找了一个新的小赵来。这个时候,技术总监一看新来的小伙子挺有出息,而且一周做一个淘宝,技术总监心里难道没点逼数么,所以任务到技术总监那的时候就不往下传达了,免得小赵压力太大就跑路了,招个好的技术本来就是那么困难,跑了就没人给他干活了对吧
修改ViewGroupB代码:

    //修改拦截返回true,不再分发事件
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupB-onInterceptTouchEvent   技术总监:肯定做不了,不做小赵压力了")
        return true
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupB-onTouchEvent   技术总监:这个需求一周做不了,给我一个阿里都搞不定,更别说一个小赵了")
        return super.onTouchEvent(event)
    }

打印结果:


image.png

老板一气之下又把技术总监给开了,老板还为了节省开支,换了一个毫无情商的技术总监,收到任务,也不问问下面的人能不能做,自己认为不能做,然后也不给上面答复能不能做。
修改代码:

    //自己就消费了事件,不给上级回传了,返回 true
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupB-onTouchEvent   技术总监:这个需求一周做不了,给我一个阿里都搞不定,更别说一个小赵了")
        return true
    }

打印结果:


image.png

技术总监没有给项目经理回复,老板等了好多天没有得到消息,自然气不过,老板二话不说把项目经理给开了,虽然是技术总监没给回复,但是你自己不会催嘛?然后老板又招了一个新的项目经理,但是这个项目经理更是扯淡,成天吊儿郎当的,收到任务不给分发也不处理,收到就收到了...
修改ViewGroupA代码:

    //分发中直接返回true
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.e("XYX", "ViewGroupA-dispatchTouchEvent   项目经理:我知道了...")
        return true    //或者return false
    }

打印结果:


image.png

这么下去,老板就要开始自闭了...

总结一下:

1、View和ViewGroup的区别在于ViewGroup中多一个onInterceptTouchEvent()拦截方法
2、三个方法默认都返回false,分发流程:dispatchTouchEvent -》onInterceptTouchEvent -》下级 dispatchTouchEvent -》onInterceptTouchEvent -》 ...直到 View的dispatchTouchEvent -》onTouchEvent -》上级onTouchEvent...直到Activity的onTouchEvent -》事件分发结束
3、在onTouchEvent方法中,如果返回true,则消费事件,不再回传给上级
4、在onInterceptTouchEvent方法中,如果返回true,则拦截事件,回调自身onTouchEvent方法,再根据onTouchEvent方法返回值判断是否回调上级的onTouchEvent
5、在dispatchTouchEvent方法中,如果直接返回true / false,不使用super. dispatchTouchEvent(ev)的话,则分发结束,不再回调自身onTouchEvent方法,。
注意:dispatchTouchEvent 方法返回的true和false的区别是true会执行action_down、action_move和action_up,而如果直接返回false只会执行到action_down。有兴趣的话,可以看一下dispatchTouchEvent方法的源码,这个方法中会在action_down时调用 dispatchTransformedTouchEvent方法,这里不详细说明了。

问:View什么会有dispatchTouchEvent方法?View事件相关的各个方法的调用顺序是怎样的?

答:ViewGroup有ChildView,所以一定需要分发,View还可以注册很多事件监听器:onClick、onLongClick、onTouch等,这些监听器的的管理也需要用dispatchTouchEvent管理。所以View相关的各个方法也就是这几个,它们的调用顺序:
1、onClickListener单击事件需要有两个事件(ACTION_DOWN、ACTION_UP)即按下和抬起操作才会产生onClickListener事件
2、onLongClickListener长按事件只需要ACTION_DOWN事件,但是需要长时间按住才会产生,所以onLongClickListener事件会比onClickListener事件之前。
3、onTouchListener触摸事件,只需要触摸即可产生,其实也是ACTION_DOWN事件,如果注册了触摸事件,消费了就不会有onLongClickListener,所以onTouchListener应该排在onLongClickListener之前。
4、onTouchEvent View自身的处理事件,也是一种触摸事件,但是我们自己注册的触摸事件会排在它的前面。

总结一下就是:自己注册的触摸事件(onTouchListener)-》自身的处理事件(onTouchEvent)-》长按事件(onLongClickListener)-》单击事件(onClickListener)

代码示例一下,还是刚刚那个,新建一个ViewB,给B设置各类事件:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //设置长按事件
        view_b.setOnLongClickListener {
            Log.e("XYX", "OnLongClickListener")
            false
        }
        //设置点击事件
        view_b.setOnClickListener {
            Log.e("XYX", "OnClickListener")
        }
        //设置触摸事件
        view_b.setOnTouchListener { _, event ->
            var str = ""
            when(event.action){
                MotionEvent.ACTION_DOWN->{
                    str = "ACTION_DOWN"
                }
                MotionEvent.ACTION_UP->{
                    str = "ACTION_UP"
                }
            }
            Log.e("XYX", "OnTouchListener--$str")
            false
        }
    }
}


class ViewB : View {

    /**
     * 在xml布局文件中使用时自动调用
     */
    constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {

    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        var str = ""
        when(event?.action){
            MotionEvent.ACTION_DOWN->{
                str = "ACTION_DOWN"
            }

            MotionEvent.ACTION_UP->{
                str = "ACTION_UP"
            }
        }
        Log.e("XYX", "onTouchEvent--$str")
        return super.onTouchEvent(event)
    }
}

打印结果:


image.png

View所有的事件分发都在dispatchTouchEvent方法中,看一下源码:

public boolean dispatchTouchEvent(MotionEvent event) {
    //省略大量代码...
    boolean result = false; // result 为返回值,主要作用是告诉调用者事件是否已经被消费。
    if (onFilterTouchEventForSecurity(event)) {
        /** 
         * 如果设置了OnTouchListener,并且当前 View 可点击,就调用监听器的 onTouch 方法,
         * 如果 onTouch 方法返回值为 true,就设置 result 为 true。
         */
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        /** 
         * 如果 result 为 false,则调用自身的 onTouchEvent。
         * 如果 onTouchEvent 返回值为 true,则设置 result 为 true。
         */
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
}

//看看onTouchEvent方法:
public boolean onTouchEvent(MotionEvent event) {
    //省略大量代码...
    final int action = event.getAction();
    // 检查各种 clickable
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                //...
                removeLongPressCallback();  // 移除长按
                //...
                performClick();             // 检查单击
                //...
                break;
            case MotionEvent.ACTION_DOWN:
                //...
                checkForLongClick(0);       // 检测长按
                //...
                break;
            //...
        }
        return true;                        // ◀︎表示事件被消费
    }
    return false;
}

这里:
1、如果设置了OnTouchListener,并且View可点击,就会调用OnTouchListener.onTouch方法,判断onTouch返回值,作为dispatchTouchEvent方法的最后的返回值。
2、如果上面的返回值为false,就会执行View自身的onTouchEvent方法,并且如果onTouchEvent方法返回true,则将dispatchTouchEvent方法设置最后的返回值为true
3、onTouchEvent方法中,检查View是否可以点击(clickable、long_clickable、context_clickable)等,在DOWN事件下检测是否是长按、UP事件下移除长按,同时检测是否单击
4、在onTouchEvent代码中,在第一个判断clickable时,代码块中直接就return true,表明只要View设置了clickable为true,则View自身就会消费事件 这也是我们在开发中常见的在View或者ViewGroup的上层再加一个View时,只要设置了点击事件或clickable时,点击上层下层是不会响应的机制。如:

<RelativeLayout
    android:background="#CCC"
    android:id="@+id/layout"
    android:onClick="myClick"
    android:layout_width="200dp"
    android:layout_height="200dp">
    <View
        android:clickable="true"
        android:layout_width="200dp"
        android:layout_height="200dp" />
</RelativeLayout>

myClick的点击方法永远不会执行,因为View设置了clickable为true,不会回调Relativelayout的onTouchEvent方法

重点重点重点,重要要说三遍:所以View事件相关的各个方法调用顺序为:自己注册的触摸事件(onTouchListener)-》自身的处理事件(onTouchEvent)-》长按事件(onLongClickListener)-》单击事件(onClickListener)

上面的面试问题其实也已经分析完毕了,既然这里对View事件分发的源码分析了,接下来就顺带看看ViewGroup的事件分发源码吧。其实ViewGroup和View非常的类似,只不过是增加了拦截之类的:

    //ViewGroup中的dispatchTouchEvent方法
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //省略很多代码...
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 处理第一次ACTION_DOWN事件
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 当开始一个新的触摸手势时,扔掉所有以前的状态。
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 检查是否需要拦截
            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); // 恢复操作,防止被更改
                } else {
                    intercepted = false;
                }
            } else {
                // 没有目标来处理该事件,而且也不是一个新的事件事件(ACTION_DOWN), 进行拦截。
                intercepted = true;
            }

            // 判断事件是否是针对可访问的焦点视图
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // 检查事件是否被取消(ACTION_CANCEL)
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // 如果需要,更新指针向下的触摸目标列表
            final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                    && !isMouseEvent;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                // 如果事件是针对可访问性焦点视图,我们将其提供给具有可访问性焦点的视图。
                // 如果它不处理它,我们清除该标志并像往常一样将事件分派给所有的 ChildView。 
                // 我们检测并避免保持这种状态,因为这些事非常罕见。
                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;

                    // 清除此指针ID的早期触摸目标,防止不同步。
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        //获取触摸位置
                        final float x =  isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y = isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // 查找可以接受事件的 ChildView
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //遍历所有子 View,查找可以接收事件的子View。
                        //从后往前查找,也遵循了覆盖在目标View的上方点击,先分发给最上层的View
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                //根据触摸位置找到目标View,则直接跳出当前循环 break
                                // 孩子已经在自己的范围内受到了触碰
                                // 除了它正在处理的指针之外,还给它一个新指针
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            //重置状态
                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // 孩子想在自己的范围内得到事件
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex指向预先排序的列表,找到原始索引
                                    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;
                            }

                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // // 没有找到 ChildView 接收事件
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // 派遣部队去接触目标。
            if (mFirstTouchTarget == null) {
                // 没有接触目标,所以把这当作一个普通的看法。
                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 {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

            // 如果需要,更新指针向上或取消的触摸目标列表。
            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);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

实际上上面的源码可能我们很多都不需要关注,重要的点在于:
1、判断是否需要拦截,拦截则直接调用自己的onTouchEvent方法,没有拦截则交给子 View,子View是否消费,如果没有消费则回调自身的onTouEvent方法
2、自身是否需要这个事件,如果不需要则询问子 View是否需要,如果不需要则回调自身的onTouchEvent方法
3、子 View有很多,通过循环遍历获取子 View,手指触摸点的位置是哪个子 View,就把事件分发给它,如果当前位置下有多个View重叠,则从上往下看是否需要事件(是否注册了事件或设置了clickable=true)
4、通过第三点也可以知道,只要设置clickable=true(设置可点击,不会给下层再分发事件),则事件就会被消费。

上一篇下一篇

猜你喜欢

热点阅读