千里马 android framework之MotionEvent.ACTION_CANCEL怎么产生-讨厌的android触摸面试题

Posted Android高级知识分享官

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了千里马 android framework之MotionEvent.ACTION_CANCEL怎么产生-讨厌的android触摸面试题相关的知识,希望对你有一定的参考价值。

hi,粉丝朋友!
大家对于MotionEvent.ACTION_CANCEL这个cancel事件是不是感觉又熟悉又陌生,熟悉是因为经常在onTouch识别触摸事件时候会把它和ACTION_UP放在一块处理,基本停留在字面意思理解为 “”取消“”
新课程优惠获取请加入qq群:422901085
Android手机大厂Framework系统-Input系统专题实战课

[入门课,实战课,跨进程专题
ps需要学习深入framework课程和课程优惠

ACTION_CANCEL触发场景和原因:

customTextView.setOnTouchListener(new View.OnTouchListener() 
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) 
        Log.i("test2"," onTouch motionEvent = " + motionEvent);
        if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP ) 
            Log.i("test2"," onTouch ACTION_CANCEL = " ,new Exception());
        
        return true;
    
);

所以这里本节课就来带大家深入理解这里ACTION_CANCEL

首先我们来分析应用触摸最常见的ACTION_CANCEL情况:

这个就是我们常见的一个父布局MyLayout,它装载了2个子控件textview1和textview2.假设一个触摸事件到来,我们知道触摸事件的传递顺序就是:父布局 – 》 子布局
这样的一个顺序,也就是其实事件父布局是具有完全的决策权利来是否给子布局,可以通过方法onInterceptTouchEvent。
接下来我们又来看看MotionEvent,一个正常完整触摸应该是怎么个顺序呢?
ACTION_DOWN – > ACTION_MOVE --> ACTION_UP
所以说我们控件正常事件处理就是最少要有DOWN --》UP两个事件,才代表事件介绍,那么你会问如果只有DOWN,和MOVE是否可以呢?答案:当然是不可以的
为啥呢?
大家可以想一下这样一个场景,你是一个按钮,来了触摸事件DOWN了后,你把按钮变成selected状态了,你本来一直等值来个UP事件来变回正常状态,如果系统发生依次你没有收到UP会怎么样呢?那就是你的应用按钮就永远处于选中状态无法取消,一直到进程关闭(当然说是正常你没有特别处理情况)。

所以有了以上基础后,大家就知道有了DOWN事件一般都要有UP事件才算完整,但是有一些场景他就是可能收到了DOWN,后面收不到UP了怎么办?
比如看如下例子:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) 
        Log.i("test2","onInterceptTouchEvent ev = " + ev);
        if (ev.getAction() == MotionEvent.ACTION_MOVE ) //我们不拦截DOWN事件,但是拦截MOVE事件,父布局一拦截,子控件就会收到ACTION_CANCEL
            return true;
        
        return super.onInterceptTouchEvent(ev);
    

这种情况我们父布局只并没有拦截DOWN事件,所以DOWN传递给你textview1,但是因为在MOVE时候我们父布局拦截了,即说明事件不再传递给子控件了textview1,那么这个是不是就是textview1收到了DOWN,但是收不到UP情况,不完整了,那该怎么办?

这个时候其实就是今天重点介绍的ACTION_CANCEL出厂,他就是来帮忙解决上面的因为父布局中间进行了触摸事件拦截,但是子布局又要一个完整触摸过程,那么就需要传递一个触摸事件给子控件,那么传递什么合适?
大家肯定会想UP最合适,UP不就可以了?但是你要想想用户还没有UP啊?如果传递了UP是不是也可能会有问题,所以这时候就是传递ACTION_CANCEL,它代表是取消,即代表这个触摸事件到此被取消结束了。控件收到需要自己做对应的扫尾工作,保证事件完整。

上面我们已经对ACTION_CANCEL触发场景和原因已经清楚,接下来看看源码是怎么处理的:

ACTION_CANCEL出现源码分析

app代码:
layout.xml

     android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingTop="100dp"
        >
        <TextView
            android:id="@+id/custom_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello CustomLayout !"
            ></TextView>

    </com.example.anrdemo.CustomLayout>

代码端对onTouch的事件进行监听:

   TextView customTextView = (TextView)findViewById(R.id.custom_text);
        customTextView.setOnTouchListener(new View.OnTouchListener() 
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) 
                Log.i("test2"," onTouch motionEvent = " + motionEvent);
                if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP ) 
                //变成了CANCEL后打印一下堆栈,追踪框架哪里进行了改变
                    Log.i("test2"," onTouch ACTION_CANCEL = " ,new Exception());
                
                return true;
            
        );
2021-11-27 13:50:06.911 7010-7010/com.example.anrdemo I/test2:  onTouch ACTION_CANCEL = 
    java.lang.Exception
        at com.example.anrdemo.MainActivity$2.onTouch(MainActivity.java:46)
        at android.view.View.dispatchTouchEvent(View.java:13949)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:465)
        at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1849)
        at android.app.Activity.dispatchTouchEvent(Activity.java:4011)
        at com.example.anrdemo.MainActivity.dispatchTouchEvent(MainActivity.java:72)
        at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
        at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
        at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:423)
        at android.view.View.dispatchPointerEvent(View.java:14212)
        at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5652)
        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5455)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4958)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5011)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4977)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5117)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4985)
        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5174)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4958)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5011)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4977)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4985)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4958)
        at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7675)
        at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7644)
        at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7605)
        at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7800)
        at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:188)
        at android.view.InputEventReceiver.nativeConsumeBatchedInputEvents(Native Method)
        at android.view.InputEventReceiver.consumeBatchedInputEvents(InputEventReceiver.java:178)
        at android.view.ViewRootImpl.doConsumeBatchedInput(ViewRootImpl.java:7751)
        at android.view.ViewRootImpl$ConsumeBatchedInputRunnable.run(ViewRootImpl.java:7824)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:967)
        at android.view.Choreographer.doCallbacks(Choreographer.java:791)
        at android.view.Choreographer.doFrame(Choreographer.java:719)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:952)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)

这里就打印出来了框架调用到onTouch中MotionEvent变成CANCEL的流程,方便我们进行源码分析。
具体大家可以自己取追踪,这里我这边列出结果:
base/core/java/android/view/ViewGroup.java


            // Check for interception.
            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); // restore action in case it was changed
                 else 
                    intercepted = false;
                
             

这里onInterceptTouchEvent会调用到我们CustomLayout的onInterceptTouchEvent,我们识别到了如果MOVE就return true,所以intercepted这个时候也是true。

final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits))

这里cancelChild就变成了true,所以

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) 
        final boolean handled;

        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) //根据前面cancelChild变成了CANCEL
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) 
                handled = super.dispatchTouchEvent(event);
             else 
                handled = child.dispatchTouchEvent(event);
            
            event.setAction(oldAction);
            return handled;
        
//---省略部分

        

从以上可以看出,结合我们Input专题学习知识,我们知道触摸事件InputDispatcher传递给App实际还是ACTION_MOVE,但是app进程ViewGroup的策略把事件变成了ACTION_CANCEL来保证事件的完整性。
总结流程图:

以上是关于千里马 android framework之MotionEvent.ACTION_CANCEL怎么产生-讨厌的android触摸面试题的主要内容,如果未能解决你的问题,请参考以下文章

android framework系统源码分析之dumpsys原理分析-千里马带你学framework

android framework开发之广播broadcast源码分析2-千里马

android framework开发之广播broadcast源码分析2-千里马

千里马android framework实战开发-binder驱动之oneway导致的transaction failed

千里马android framework实战开发-binder驱动之oneway导致的transaction failed

千里马android framework实战开发-binder驱动之oneway导致的transaction failed