事件分发之-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 相关问题
相关推荐
艾小逗1 小时前
uniapp下载&打开实现方案,支持安卓ios和h5,下载文件到指定目录,安卓文件管理内可查看到
android·ios·uni-app·uniapp文件下载
追梦-北极星1 小时前
android系统查找应用包名以及主activity:
android
guishou先生2 小时前
手机联系人 查询 添加操作
android
我又来搬代码了3 小时前
【Android】application@label 属性属性冲突报错
android
机器视觉小小测试员3 小时前
自动化测试工具Ranorex Studio(七十五)-录制ANDROID测试
android·测试工具·自动化
van叶~5 小时前
仓颉语言实战——2.名字、作用域、变量、修饰符
android·java·javascript·仓颉
m0_748239335 小时前
【PHP】部署和发布PHP网站到IIS服务器
android·服务器·php
小林爱5 小时前
【Compose multiplatform教程14】【组件】LazyColumn组件
android·前端·kotlin·android studio·框架·多平台
牧杉-惊蛰6 小时前
html转PDF
android·pdf
yangfeipancc12 小时前
数据库-用户管理
android·数据库