事件分发之-ACTION_CANCEL

前言

回想我们刚开始学Android事件分发机制时,都是从这三个方法 dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()开始,也是从这三个方法结束,学完后有一种似懂非懂的感觉。在工作和面试时,提到事件分发也都是这三个方法,然后再多问一句,对于事件分发,你还了解些什么,就陷入了深思,再也组织不起来完整的语句。

  • 事件的源头在哪里
  • ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL 等事件又是如何变换或产生的
  • 对于小窗模式以及不同分辨率的手机事件的坐标又是如何变换的
  • 多指触摸事件是何如处理的
  • 事件处理完之后,它又要到哪里去...

这篇文档将会以ACTION_CANCEL 如何产生的为源头,逐步展开。


结论

  • 当一子view收到 ACTION_DOWN事件之后,还未收到ACTION_UP事件之间,父控件通过某些方式拦截了子view的后续事件,那么父控件为了让其子view事件闭环,会将收到的一事件变换成ACTION_CANCEL事件,发送给子view。
  • 当一view正在处理事件时,此时来了电话打开了系统电话页面或者突然点击HOME键会到了桌面,这时候系统会直接给该view发送一个ACTION_CANCEL事件,使其完整。

结论分析(1)

测试代码

布局UI

布局 xml

在MainActivity中引用

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<com.example.myapplication.CanCelEventTestLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="button1" />

</com.example.myapplication.CanCelEventTestLayout>

CanCelEventTestLayout

很简单的继承于FrameLayout,重写onInterceptTouchEvent 方法

less 复制代码
public class CanCelEventTestLayout extends FrameLayout {
    public CanCelEventTestLayout(@NonNull Context context) {
        super(context);
    }

    public CanCelEventTestLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CanCelEventTestLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }
}

MainActivity

scala 复制代码
public class MainActivity extends AppCompatActivity {

    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        Button button1 = findViewById(R.id.button1);
        button1.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e("xueren", "button1 event : " + event);
                if (event.getAction() == MotionEvent.ACTION_CANCEL) {
                    Log.e("xueren", "button1 MotionEvent.ACTION_CANCEL  : ", new Throwable());
                }
                return true;
            }
        });
    }


}
  • 为 button1 设置 setOnTouchListener, 在其 ****onTouch ****方法中打印出两行日志,手指在 button1上触摸、点击,观察事件MotionEvent的变化
  • 正常情况下触摸、点击,Button1只会收到 ACTION_DOWN、ACTION_UP、ACTION_MOVE,甚至手指滑出button1的范围,也依然会收到ACTION_MOVE、ACTION_UP(这块内容后面会单独分析)
  • 日志截图如下

所以 :这种正常情况下 MotionEvent.ACTION_CANCEL 是不会产生的

修改 CanCelEventTestLayout.onInterceptTouchEvent

修改button1的父布局 CanCelEventTestLayout 的 onInterceptTouchEvent方法,当 ACTION_MOVE事件产生时,对其拦截

scala 复制代码
public class CanCelEventTestLayout extends FrameLayout {
    //省略其构造方法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e("xueren", "CanCelEventTestLayout event : " + ev);
        if(ev.getAction() == MotionEvent.ACTION_MOVE){
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

修改后再次在button1上触摸,log日志如下

  • CanCelEventTestLayout event : MotionEvent { action=ACTION_DOWN, actionButton=0, id0=0, x0=557.0, y0=1043.0, toolType0=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=160248436, downTime=160248436, deviceId=6, source=0x1002, displayId=0 }
  • button1 event : MotionEvent { action=ACTION_DOWN, actionButton=0, id0=0, x0=154.0, y0=49.0, toolType0=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=160248436, downTime=160248436, deviceId=6, source=0x1002, displayId=0 }
  • CanCelEventTestLayout event : MotionEvent { action=ACTION_MOVE, actionButton=0, id0=0, x0=556.0, y0=1043.0, toolType0=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=160248713, downTime=160248436, deviceId=6, source=0x1002, displayId=0 }
  • button1 event : MotionEvent { action=ACTION_CANCEL, actionButton=0, id0=0, x0=556.0, y0=1043.0, toolType0=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=160248713, downTime=160248436, deviceId=6, source=0x1002, displayId=0 }

在button1的日志中,当发现有ACTION_CANCEL事件时,会通过Log.e("xueren", "button1 MotionEvent.ACTION_CANCEL : ", new Throwable()) 打印出堆栈

在 button1 event 的日志中发现了 ACTION_CANCEL 事件

所以 :结论一通过代码复现了ACTION_CANCEL 事件

在该例子中,通过父控件onInterceptTouchEvent方法拦截了子控件 MotionEvent.ACTION_MOVE 后面事件,那么在子控件中将会收到ACTION_CANCEL事件作为结束事件

结论已通过一个小例子验证,不过还有1个小问题待讨论

  • button1中 ACTION_CANCEL 事件是从哪里产生的

观察上面的日志,可以发现首先产生 ACTION_DOWN,先经过CanCelEventTestLayout 再到 button1,通过看MotionEvent.eventTime 字段,可知分别经过父控件和子控件的ACTION_DOWN事件,其实是同一时间产生的。

再看后续,ACTION_MOVE 事件流向了 CanCelEventTestLayout,随后在 button1中发现了 ACTION_CANCEL,

该 ACTION_CANCEL 事件和 CanCelEventTestLayout 中的 ACTION_MOVE 事件 MotionEvent.eventTime 字段也是一样的~ 它们也是同一时间产生的,属于一个事件流。

所以父控件中的 ACTION_MOVE事件,到了子控件中就变成了 ACTION_CANCEL

源码分析

在打印的堆栈中可以注意到一个方法: dispatchTransformedTouchEvent

csharp 复制代码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
  
   //省略其他代码
   ........
   ........
   ........

}

dispatchTransformedTouchEvent 方法是在 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java) 中

调用的。

注意dispatchTransformedTouchEvent 方法的前两个参数 MotionEvent event, boolean cancel,如果 cancel 为true时,将会通过 event.setAction(MotionEvent.ACTION_CANCEL) 将原有事件的 action 设置为 ACTION_CANCEL,当传递给子view后,又重新设置为原先的 action。

其实到这里结论一的ACTION_CANCEL事件的产生已经分析完了,但是还缺点逻辑没有补全,就是还是没有明确点透父控件的onInterceptTouchEvent在ACTION_MOVE 事件到来时,返回 true 与dispatchTransformedTouchEvent中参数cancel 为true的联系。 感觉再写这个逻辑就有点啰嗦了,就不写了。

(感兴趣的同学去看看ViewGroup.dispatchTouchEvent 中 mFirstTouchTarget 和 onInterceptTouchEvent以及 dispatchTransformedTouchEvent的调用逻辑就很清楚了 )

遗留问题:

  • 正常情况下触摸、点击,Button1只会收到 ACTION_DOWN、ACTION_UP、ACTION_MOVE,甚至手指滑出button1的范围,也依然会收到ACTION_MOVE、ACTION_UP(这块内容后面会单独分析)
  • mFirstTouchTarget 相关问题
相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui