Android中ACTION_CANCEL的触发时机

转载请以连接形式标明出处: 本文出自:103style的博客java

code base on android-28android


目录

  • 结论
  • 测试代码
  • 源码分析

结论

  • 当子控件处理了 ACTION_DOWN 事件, 可是父控件拦截了 ACTION_MOVE 事件,子控件不会收到ACTION_CANCEL
  • 当子控件处理了 ACTION_DOWN 事件, 可是父控件拦截了 ACTION_UP 事件,当再次点击父控件,子控件会收到 ACTION_CANCEL

即 子控件 在处理了 ACTION_DOWN 以后,若是没有收到 ACTION_UP 事件,则在下次点击父控件的时候会收到 ACTION_CANCEL 事件。bash


测试代码

新建一个项目, 修改activity_main.xml以下:ide

<?xml version="1.0" encoding="utf-8"?>
<包名.TestLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <包名.TestTextView
        android:id="@+id/test"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        android:background="@color/colorPrimary" />
</包名.TestLayout>
复制代码

添加 TestLayout.javaTestTextView.java源码分析

public class TestLayout extends FrameLayout {
    public TestLayout(@NonNull Context context) {
        super(context);
    }
    public TestLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public TestLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (
                ev.getAction() == MotionEvent.ACTION_MOVE
                        || ev.getAction() == MotionEvent.ACTION_UP
        ) {
            return true;
        }
        return super.dispatchTouchEvent(ev);
    }
}
复制代码
public class TestTextView extends TextView {
    private static final String TAG = "TestTextView";
    public TestTextView(Context context) {
        super(context);
    }
    public TestTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public TestTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, System.currentTimeMillis() + " onTouchEvent: action = " + event.getAction());
        return true;
    }
}
复制代码
  • 首先咱们注释 TestLayout|| ev.getAction() == MotionEvent.ACTION_UP 这句,运行点击中间的子控件,而后移动一下,再抬起,而后重复一次,打印日志以下:测试

    TestTextView: 1571728312400  onTouchEvent: action = 0
    TestTextView: 1571728314213  onTouchEvent: action = 1
    TestTextView: 1571728316088  onTouchEvent: action = 0
    TestTextView: 1571728318113  onTouchEvent: action = 1
    复制代码

    发现只收到 ACTION_DOWNACTION_UP 事件。ui

  • 而后咱们把 TestLayout|| ev.getAction() == MotionEvent.ACTION_UP 这句恢复,重复操做,发现日志图下:spa

    TestTextView: 1571728458713  onTouchEvent: action = 0
    TestTextView: 1571728460379  onTouchEvent: action = 3
    TestTextView: 1571728460379  onTouchEvent: action = 0
    复制代码

    根据时间戳咱们知道在第二次点击时,才会先收到ACTION_CANCEL,再收到 ACTION_DOWN日志


源码分析

  • 第一种只拦截 ACTION_MOVE 咱们很好分析,由于事件是从上面一层一层传下来的,当上层拦截了 消耗了 ACTION_MOVE 事件以后,就不会往下传递了,咱们的操做是由一个 ACTION_DOWN 多个 ACTION_MOVE 和 一个 ACTION_UP 组成的, 因此子控件只能收到 ACTION_DOWNACTION_UPcode

  • 第二种由于拦截了ACTION_MOVEACTION_UP 事件,因此子控件只能收到 ACTION_DOWN

而后在每次收到 ACTION_DOWN 的时候,ViewGroup 都会去清除以前的状态。 即下面的 cancelAndClearTouchTargets(ev)dispatchTransformedTouchEvent(...)child.dispatchTouchEvent(event),即将 MotionEvent.ACTION_CANCEL 传给子控件 。

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);//清除以前的状态
        }
    }
}
private void cancelAndClearTouchTargets(MotionEvent event) {
    if (mFirstTouchTarget != null) {
        boolean syntheticEvent = false;
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            //传递ACTION_CANCEL给子View
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
    }
}
private boolean dispatchTransformedTouchEvent(...) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        // cancel 为 true
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            //传递ACTION_CANCEL给子View
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
}
复制代码

那这个 mFirstTouchTarget 是什么,何时被赋值的呢?

private TouchTarget mFirstTouchTarget;
private static final class TouchTarget {
    public View child;
    public TouchTarget next;
}
复制代码

mFirstTouchTarget 是一个保存处理了事件的 子View链表 结构。

赋值也在 dispatchTouchEvent 中,在 dispatchTransformedTouchEvent(...) 中 判断子View是否处理了事件,处理了则添加到 mFirstTouchTarget 链表的头部。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onFilterTouchEventForSecurity(ev)) {
        if (!canceled && !intercepted) {
            if (...) {
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(...);
                        final View child = getAndVerifyPreorderedView(...);
                        //判断子View是否处理了事件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            //把处理了事件的子View 添加到链表头部
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
    ...
}
//判断子View是否处理了事件
private boolean dispatchTransformedTouchEvent(...) {
    final boolean handled;
    final int oldAction = event.getAction();
    final MotionEvent transformedEvent;  
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    transformedEvent.recycle();
    return handled;
}
//添加到mFirstTouchTarget链表的头部
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
复制代码

因此流程大体就是, 当第一次点击 TestLayout, 因为 TestTextView 消耗了 ACTION_DOWN 事件,而后被保存在 TestLayoutmFirstTouchTarget 链表中, 在第二次点击的时候,经过 cancelAndClearTouchTargets 方法,遍历 mFirstTouchTarget 链表,将 ACTION_CANCEL 事件传递给子view。


若是以为不错的话,请帮忙点个赞呗。

以上


扫描下面的二维码,关注个人公众号 Android1024, 点关注,不迷路。

Android1024
相关文章
相关标签/搜索