ViewGroup学习之触摸事件分发

本文详细解析了Android事件机制之来龙去脉,着重阐述了DecorView、Activity与ViewGroup之间的事件传递过程。通过具体分析Activity的事件处理逻辑,展示了如何在自定义控件中实现事件拦截与分发。最后,提供了一个示例演示了事件在不同组件间的传递路径,帮助开发者理解事件处理机制。
接上一篇 android事件机制之来龙去脉,从WindowManagerService跟踪到DecorView(ViewGroup)和Activity, Activity的事件没有分发的概念,只是单纯的条用onTouchEvent(ev), 我们care的是ViewGroup的事件分发,以此来完善 android事件机制之来龙去脉,也为以后定制自己的控件做准备。其实很多博客都有写ViewGroup事件到底如何分发和拦截,在此就抛砖引玉,不喜勿喷。

  在上一篇尾,DecorView并没有重写ViewGroup的dispatchTouchEvent方法, 所以事件传到Activity.dispatchTouchEvent:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

然后由getWindow().superDispatchTouchEvent(ev)传递到了DecorView.dispatchTouchEvent(ev)即ViewGroup.dispatchTouchEvent(ev), 由此Activity拦截View的事件相当的简单, 只要重写dispatchTouchEvent(ev)就可以达到,view拦截Activity的onTouchEvent也很简单, 只要Activity布局的Root消耗掉事件就行了。下面具体分析一下ViewGroup.dispatchTouchEvent(ev):

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    /**
     * 一个完整的事件由ACTION_DOWN ACTION_MOVE... ACTION_UP组成
     * 事件的开端只有一个ACTION_DOWN, 事件的结束有ACTION_UP, ACTION_CANCEL
     *
     * 此时的ActionEvent可能是:
     *
     * ACTION_DOWN
     * ACTION_MOVE
     * ACTION_UP
     * ACTION_CANCEL
     */

    /**
     * 单独对ACTION_DOWN对其进行动作拦截判断, ACTION_DOWN是一个事件的开始
     * 如果事件的开始就被拦截了, 后面的一系列动作就不用再分发下去了
     */
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (action == MotionEvent.ACTION_DOWN) {
         if (mMotionTarget != null) {
             mMotionTarget = null;
         }
        /* 如果不允许拦截 或者没有对ACTION_DOWN拦截  */
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ...
            // 寻找焦点树把事件传递到子View中
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        ...
                        if (child.dispatchTouchEvent(ev))  {
                            /**
                             * 焦点树消耗了ACTION_DOWN事件, 得到一个消耗ACTION_DOWN(事件开端)的Target
                             * 此时的ACTION_DOWN已经消耗了, 没有必要往下面传递了
                             */
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }

    ...

    final View target = mMotionTarget;

    /**
     * 此时的ActionEvent可能是:
     *
     * ACTION_DOWN
     * ACTION_MOVE
     * ACTION_UP
     * ACTION_CANCEL
     *
     * 对于ACTION_DOWN为何还能传递到这里, 总的原因是ACTION_DOWN事件没有被消耗, 有好几种情况:
     *
     * 1、ACTION_DOWN被拦截了, 压根就没有执行上面的if
     * 2、ACTION_DOWN没有被拦截, 此时可能是
     *      a、事件往焦点树传递到了子View, 但是焦点树没有消耗掉ACTION_DOWN
     *      b、事件找不到有焦点的子View, 即 焦点就是本身
     *
     */

    /**
     * 如果事件没有被子焦点消耗, 那么就交给ViewGroup处理
     * 调用View.dispatchTouchEvent(ev)
     *
     * 这里是一个常规的处理, 即事件到达时需要判断子控件树是否消耗了事件, 如果没有消耗则查看
     * 自己会不会消耗掉事件
     *
     * 到这一步ACTION_DOWN已经处理完了
     */
    if (target == null) {
        ...
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 事件传递到这一步, 此时target != null, 说明
     *
     * 1、ACTION_DOWN没有被拦截
     * 2、有一个子View已经消耗了ACTION_DOWN动作, 即mMotionTarget
     */

    /**
     * 此时的ActionEvent可能是:
     *
     * ACTION_MOVE
     * ACTION_UP
     * ACTION_CANCEL
     *
     * 如果此时对以上3个可能动作有拦截, 其实ACTION_CANCEL没必要拦截
     *
     * 此时如果mMotionTarget把ACTION_DOWN消耗了, 其必将有事件的结尾信号
     * 但是此时却把后续事件拦截了, 那就对mMotionTarget事件cancel了
     */
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        if (!target.dispatchTouchEvent(ev)) {

        }
        mMotionTarget = null;
        return true;
    }

    /* 完整的事件结束, 清除mMotionTarget */
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        mMotionTarget = null;
    }

    ...

    /**
     * 此时的ActionEvent可能是:
     *
     * ACTION_MOVE
     * ACTION_UP
     * ACTION_CANCEL
     *
     * 事件分发到此处, 说明ViewGroup没有对任何事件拦截, 并且有一个子View已经消耗了ACTION_DOWN
     * 既然已经消耗了ACTION_DOWN, 后续事件也应该处理
     */
    return target.dispatchTouchEvent(ev);
}

 通过上面的分析, 总结一下onInterceptTouchEvent事件拦截特性, 以及ViewGroup事件分发的特性

onInterceptTouchEvent:

1、当RootView收到事件来临通知时候, 此时如果是ACTION_DOWN, 检查是否拦截, 不拦截则发送消息给下层,试探下层是否消耗ACTION_DOWN

2、如果下层不消耗ACTION_DOWN, 则后续事件将不会发送给下层, 此时下层只能接收到一个ACTION_DOWN的探测

3、谁消耗掉ACTION_DOWN, 后续事件将会直接派送给

 

下面写一个Demo测试:

RootView
/**
 * LinearLayout对dispatchTouchEvent没有任何修改
 *
 * @author bxwu
 *
 */
public class RootView extends LinearLayout {
    private static final String TAG = "RootView";

    public RootView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG, "onInterceptTouchEvent-------->ACTION_DOWN");
            return false;

        case MotionEvent.ACTION_MOVE:
            Log.d(TAG, "onInterceptTouchEvent-------->ACTION_MOVE");
            return false;

        case MotionEvent.ACTION_UP:
            Log.d(TAG, "onInterceptTouchEvent-------->ACTION_UP");
            return false;

        case MotionEvent.ACTION_CANCEL:
            Log.d(TAG, "onInterceptTouchEvent-------->ACTION_CANCEL");
            return false;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG, "onTouchEvent-------->ACTION_DOWN");
            return false;

        case MotionEvent.ACTION_MOVE:
            Log.d(TAG, "onTouchEvent-------->ACTION_MOVE");
            return false;

        case MotionEvent.ACTION_UP:
            Log.d(TAG, "onTouchEvent-------->ACTION_UP");
            return false;

        case MotionEvent.ACTION_CANCEL:
            Log.d(TAG, "onTouchEvent-------->ACTION_CANCEL");
            return false;
        }
        // RootView 不消耗任何动作
        return false;
    }
}
Cell0
public class Cell0 extends LinearLayout {
    private static final String TAG = "Cell0";

    public Cell0(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG, "onInterceptTouchEvent-------->ACTION_DOWN");
            return false;

        case MotionEvent.ACTION_MOVE:
            Log.d(TAG, "onInterceptTouchEvent-------->ACTION_MOVE");
            return false;

        case MotionEvent.ACTION_UP:
            Log.d(TAG, "onInterceptTouchEvent-------->ACTION_UP");
            return false;

        case MotionEvent.ACTION_CANCEL:
            Log.d(TAG, "onInterceptTouchEvent-------->ACTION_CANCEL");
            return false;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG, "onTouchEvent-------->ACTION_DOWN");
            return false;

        case MotionEvent.ACTION_MOVE:
            Log.d(TAG, "onTouchEvent-------->ACTION_MOVE");
            return false;

        case MotionEvent.ACTION_UP:
            Log.d(TAG, "onTouchEvent-------->ACTION_UP");
            return false;

        case MotionEvent.ACTION_CANCEL:
            Log.d(TAG, "onTouchEvent-------->ACTION_CANCEL");
            return false;
        }
        return true;
    }
}
Cell1
layout
<com.bxwu.touch.RootView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:background="#000000" >

    <com.bxwu.touch.Cell0
        android:layout_width="200dip"
        android:layout_height="300dip"
        android:gravity="center"
        android:background="#ff0000">

        <com.bxwu.touch.Cell1
            android:layout_width="100dip"
            android:layout_height="200dip"
            android:background="#00ff00">

        </com.bxwu.touch.Cell1>

    </com.bxwu.touch.Cell0>

</com.bxwu.touch.RootView>

 

效果图如下:

 底部黑色是RootView, 中间层是Cell0, 上层绿色是Cell1

1、RootView, Cell0, Cell1不消耗任何事件也不拦截任何事件, 即所有都return false

a、ACTION_DOWN击在了黑色区域:

b、ACTION_DOWN击在了红色区域:

c、ACTION_DOWN击在了绿色区域:

此时不论你如何滑动触摸点你只能接收到ACTION_DOWN, 而没有后续事件传递过来, 这是为什么呢? 点击区域的不同产生的效果为何不一样?

事件从DecorView(如果不清楚DecorView可以看我前一篇博客)分发时,即调用ViewGroup.dispatchTouchEvent(ev), 此时的RootView是DecorView的子View(非直接子View, 中间还包了一层, 但这不影响分析,你可以把它当做直接子View), 接着按照 以上对ViewGroup.dispatchTouchEvent(ev)分析路线走, 此时DecorView发下一个ACTION_DOWN探测动作给了RootView(因为RootView在点击区域中), 探测结果是RootView并不消耗ACTION_DOWN事件, 所以DecorView对于后续事件就不会分发下来了,当然这种情况是一种畸形的生态循环。实际上原生的View在onTouchEvent(ev)会消耗事件的。

2、如果Cell1消耗掉ACTION_DOWN, 触摸红色区域, 就会变成:

为何会成这样,ViewRoot包含了Cell0, Cell0包含了Cell1, Cell1消耗了ACTION_DOWN, 对于DecorView来说就是ViewRoot这个整体消耗了ACTION_DOWN事件, 所以会继续分发后续事件,而ViewRoot以及Cell0将不会再onTouchEvent接收到事件。

情况还有很多,本人已经晕了,虽然没有列出事件拦截的例子,但是脉络已经很清楚了,修改一下例子就能够测试各种拦截。下一篇博客会根据View.dispatchTouchEvent以及View.onTouchEvent进行整体分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值