前言
回想我们刚开始学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 事件到来时,返回 true 与dispatchTransformedTouchEvent中参数cancel 为true的联系。 感觉再写这个逻辑就有点啰嗦了,就不写了。
(感兴趣的同学去看看ViewGroup.dispatchTouchEvent 中 mFirstTouchTarget 和 onInterceptTouchEvent以及 dispatchTransformedTouchEvent的调用逻辑就很清楚了 )
遗留问题:
- 正常情况下触摸、点击,Button1只会收到 ACTION_DOWN、ACTION_UP、ACTION_MOVE,甚至手指滑出button1的范围,也依然会收到ACTION_MOVE、ACTION_UP(这块内容后面会单独分析)
- mFirstTouchTarget 相关问题