咱们知道,正常情况下,一个单指接触事情的事情序列如下图所示:

面试问题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收到的事情序列如下图所示:

面试问题003-触摸事件中ACTION_CANCEL什么时候触发?

能够看到确实收到了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上拖动,随后松手,能够得到如下日志:

面试问题003-触摸事件中ACTION_CANCEL什么时候触发?

能够看到确实向CustomView补发了ACTION_CANCEL事情,以完毕当时事情序列。

这个比如是不是很有特色?结合这个比如咱们不难实现在一个翻滚布局中嵌套另一个翻滚组件的策略,以ScrollView嵌套定高TextView举例,此刻TextView接纳一组ACTION_MOVE事情,当TextView内容翻滚究竟后ScrollView onIntercepTouchEvent回来true,阻拦事情,继续翻滚整个页面。

事情序列总结

根据上文评论,咱们不难得出,针对一个View所接纳到的单指接触事情而言,其或许的事情序列如下图所示:

面试问题003-触摸事件中ACTION_CANCEL什么时候触发?