面试问题003-触摸事件中ACTION_CANCEL什么时候触发?
我们知道,正常情况下,一个单指触摸事件的事件序列如下图所示:
其起于ACTION_DOWN,终于ACTION_UP,中间伴随多个ACTION_MOVE,那么怎么确定应该把这个序列分发给那个View呢?在ViewGroup的dispatchTouchEvent会在ACTION_DOWN时深度优先遍历View树,找到消耗事件的View,然后存储在mFirstTouchTarget链表中(单指触摸时,链表中只有一个元素),随后将相关事件序列分发给链表中的View进行处理。
以上是事件分发的基本理论,那么对于一个View而言,其真的能一直有效消耗整个事件序列吗?比如说,我们按住屏幕滑动,突然后台自动打开了新页面,在整个过程中我们都没有抬起手指,甚至在新页面展示时还进行了滑动,此时对旧页面而言没有ACTION_UP,事件序列怎么停止呢?它又会收到什么事件呢?另外大家都知道事件分发时ViewGroup onInterceptTouchEvent返回false,事件才会向其内部Child View分发,否则就在当前ViewGroup的onTouchEvent处理,那么如果前5秒onInterceptTouchEvent返回false,过了5秒返回true,Child View所接收到的事件后续又是什么样的呢?ChildView还能收到ACTION_UP吗?此时就要介绍到ACTION_CANCEL了,针对正在处理事件的View而言,如果后续事件不再由其处理,即其失去了事件处理焦点,则会向该View分发ACTION_CANCEL事件用于标记事件结束。
接下来我们来编写代码验证一下:
单指触摸时后台自动打开新页面
private static final int MSG_START_ACTIVITY = 400;
private Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void dispatchMessage(@NonNull Message msg) {
if (msg.what == MSG_START_ACTIVITY) {
Log.d("EVENT_TEST","start DialogActivity");
startActivity(new Intent(MainActivity.this,NotifyAcLifecycleActivity.class));
return;
}
super.dispatchMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.shape_image).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("EVENT_TEST","shape_image receiveTouchEvent:"+MotionEvent.actionToString(event.getAction()));
if (MotionEvent.ACTION_DOWN == event.getAction()) {
mHandler.sendEmptyMessageDelayed(MSG_START_ACTIVITY,50);
}
return true;
}
});
}
示例代码如上所示,为shape_image设置OnTouchListener,在ACTION_DOWN时延时50ms启动新的Activity,此时shape_image收到的事件序列如下图所示:
可以看到确实收到了ACTION_CANCEL事件,以结束当前事件序列。
需要注意的是,如果NotifyAcLifecycleActivity是Dialog样式Activity或者translucent Activity,则会发生事件透传,在抬起手指前,事件仍由shape_image处理,其收到的仍然是正常完整的事件序列,起于ACTION_DOWN,终于ACTION_UP,即使已经切回道新页面。
单指在ChildView滑动时,ViewGroup onInterceptTouchEvent先返回false,再返回true
如章节标题描述,我们自定义CustomViewGroup和CustomView,在CustomViewGroup接收到前5个事件时,onInterceptTouchEvent返回false,随后的事件onInterceptTouchEvent返回true,示例代码如下:
// CustomViewGroup.java
public class CustomViewGroup extends FrameLayout {
private static final String TAG = "InterceptEventTest";
private int mEventCount = 0;
private int MAX_EVENT_COUNT = 5;
public CustomViewGroup(@NonNull Context context) {
super(context);
}
public CustomViewGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG,"CustomViewGroup dispatchTouchEvent Action is:"+MotionEvent.actionToString(ev.getAction()));
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercepted;
// 0-4这5个事件返回false不拦截
if (mEventCount < MAX_EVENT_COUNT) {
mEventCount ++;
isIntercepted = false;
} else { // 5个事件以后返回true拦截
isIntercepted = true;
}
Log.d(TAG,"CustomViewGroup onInterceptTouchEvent Action is:"+MotionEvent.actionToString(ev.getAction())+",isIntercepted:"+isIntercepted);
return isIntercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"CustomViewGroup onTouchEvent Action is:"+MotionEvent.actionToString(event.getAction()));
return super.onTouchEvent(event);
}
}
// CustomView.java
public class CustomView extends androidx.appcompat.widget.AppCompatButton {
private static final String TAG = "InterceptEventTest";
public CustomView(@NonNull Context context) {
super(context);
}
public CustomView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG,"CustomView dispatchTouchEvent Action is:"+MotionEvent.actionToString(event.getAction()));
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"CustomView onTouchEvent Action is:"+MotionEvent.actionToString(event.getAction()));
return super.onTouchEvent(event);
}
}
将CustomViewGroup和CustomView添加到布局文件中,在CustomView上拖动,随后松手,可以得到如下日志:
可以看到确实向CustomView补发了ACTION_CANCEL事件,以结束当前事件序列。
这个例子是不是很有特点?结合这个例子我们不难实现在一个滚动布局中嵌套另一个滚动组件的策略,以ScrollView嵌套定高TextView举例,此时TextView接收一组ACTION_MOVE事件,当TextView内容滚动到底后ScrollView onIntercepTouchEvent返回true,拦截事件,继续滚动整个页面。
事件序列总结
根据上文讨论,我们不难得出,针对一个View所接收到的单指触摸事件而言,其可能的事件序列如下图所示:
转载自:https://juejin.cn/post/7218532789196816445