事件分发之-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, id[0]=0, x[0]=557.0, y[0]=1043.0, toolType[0]=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, id[0]=0, x[0]=154.0, y[0]=49.0, toolType[0]=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, id[0]=0, x[0]=556.0, y[0]=1043.0, toolType[0]=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, id[0]=0, x[0]=556.0, y[0]=1043.0, toolType[0]=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 事件到来时,返回 truedispatchTransformedTouchEvent中参数cancel 为true的联系。 感觉再写这个逻辑就有点啰嗦了,就不写了。

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

遗留问题:

  • 正常情况下触摸、点击,Button1只会收到 ACTION_DOWN、ACTION_UP、ACTION_MOVE,甚至手指滑出button1的范围,也依然会收到ACTION_MOVE、ACTION_UP(这块内容后面会单独分析)
  • mFirstTouchTarget 相关问题
相关推荐
COSMOS_*2 小时前
2025最新版 Android Studio安装及组件配置(SDK、JDK、Gradle)
android·ide·jdk·gitee·android studio
jian110582 小时前
android studio Profiler性能优化,查看内存泄漏
android·性能优化·android studio
建群新人小猿5 小时前
陀螺匠企业助手——组织框架图
android·java·大数据·开发语言·容器
TheNextByte15 小时前
如何将文件从Android无线传输到 iPad
android·ios·ipad
赫萝的红苹果6 小时前
实验探究并验证MySQL innoDB中的各种锁机制及作用范围
android·数据库·mysql
叶落无痕526 小时前
Android Studio 2024.3.1 连接夜神模拟器
android·ide·android studio
玲子的猫7 小时前
安卓原生开发实现图片双指放大预览功能
android
2501_915106328 小时前
如何在iPad上高效管理本地文件的完整指南
android·ios·小程序·uni-app·iphone·webview·ipad
似霰8 小时前
AIDL Hal 开发笔记5----实现AIDL HAL
android·framework·hal
2501_915106328 小时前
iOS 成品包加固,在只有 IPA 的情况下,能做那些操作
android·ios·小程序·https·uni-app·iphone·webview