深入源码理解Android Touch事件分发机制(下篇)

本文详细解析了Android中Touch事件在ViewGroup和View中的分发机制,包括dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent等方法的工作原理,以及如何通过这些方法解决实际开发中的Touch事件冲突。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

      上文我们彻底弄清楚了onTouch、onTouchEvent、onClick这三者的区别和联系,也弄清楚Touch事件的传递原则以及事件在Activity、DecorView中的分发和传递。也给大家初步介绍了跟Touch事件分发息息相关的三个最重要的方法dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent,并给大家留下了一个疑惑:Touch事件在ViewGroup和View中是怎么分发和传递的?    那么,本篇我们将为大家解决这个疑惑,重点介绍Touch事件在ViewGroup和View中的分发机制。

      首先要带大家探究的就是ViewGroup对Touch事件的分发过程,其主要实现是在ViewGroup的dispatchTouchEvent方法中实现的,该方法可以说是相当长、相当复杂。我们一段段来分析,我们先来看一下第一段源码:

// Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

      很明显这一段是对对当前ViewGroup是否该拦截Touch事件进行判断。当当前的是DOWN事件或者当mFirstTouchTarget != null时会判断是否拦截当前事件。那么,这里读者可能有一个疑惑:mFirstTouchTarget是个什么东东???  看官别急,从后面的逻辑咱们可以看到,当ViewGroup的子View处理了Touch事件后,mFirstTouchTarget就会被赋值并指向该子View,换言之就是当ViewGroup不拦截事件并将其交给子View来处理是mFirstTouchTarget != null,一旦ViewGroup拦截事件那么mFirstTouchTarget != null就不成立了,那么当MOVE和UP事件到来的时候就不会走到ViewGroup的onInterceptTouchEvent方法中去了,整个一系列的事件将全部由该ViewGroup来处理。

     当然也有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT这个标记位,它是由requestDisallowInterceptTouchEvent方法来设置的,一般是在子View中调用。一旦FLAG_DISALLOW_INTERCEPT设置后,ViewGroup将无法拦截除了DOWN事件外的其它事件。为什么会是除了DOWN事件外的其它事件呢?因为如果是DOWN事件,那么就会重置FLAG_DISALLOW_INTERCEPT标记位,从而使子View设置的该标记位失效。所以如果是DOWN事件的话,总会调用onInterceptTouchEvent方法来询问是否拦截,这点从上述源码也能看出。通过下面的这段源码,我们能对ViewGroup处理DOWN事件有着更清晰的认识:

     

if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
      如果是DOWN事件,会停止和清除所有的TouchTarget也就是说此时mFirstTouchTarget肯定为空,并调用resetTouchState方法对FLAG_DISALLOW_INTERCEPT进行重置。至此,我们对ViewGroup的dispatchTouchEvent方法基本上已经了解清楚了,当然这还是纯粹从源码的角度来分析的,我们还得写个小demo来进行辅助验证:同样的,我们还是写一个MainActivity,里面有一个继承LinearLayout的自定义ViewGroup——TestLayout,该ViewGroup中放一个自定义的子View——TestView。

      

/**
 * Created by leevi on 16/9/1.
 */
public class TestLayout extends LinearLayout{
    private Context mContext;
    public TestLayout(Context context) {
        this(context,null);
        mContext = context;
    }

    public TestLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("TestLayout", "dispatchTouchEvent!!!!!!");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d("TestLayout", "onInterceptTouchEvent!!!!!!");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("TestLayout", "onTouchEvent!!!!!!");
        return super.onTouchEvent(event);
    }
}
       先只关注ViewGroup的情况,这种情况下,我们点击TestView,能得到如下log:


       很明显可以看出能走到onInterceptTouchEvent方法,那我们再将dispatchTouchEvent方法的返回值改成true和false看看是什么结果:

       返回值改成true的情况:


      很明显可以看出,走了两次dispatchTouchEvent方法,说明DOWN 和 UP事件都只走到ViewGroup的dispatchTouchEvent方法就不能往下走了,会父上传递给父控件的onTouchEvent方法(后面会聊到。)

      那再来看看返回值是false的情况吧:


      纳尼!!!!!!!居然只打印了一次dispatchTouchEvent,这大大出乎我们的意料吧,意思是当我们DOWN事件到来后碰到这种情况整个Touch事件就结束了,后面的MOVE和UP事件都不会处理了。

      所以我们可以得出结论,只有当ViewGroup的dispatchTouchEvent返回super.dispatchTouchEvent(event)的时候才能将事件传递下去。


      那接下来我们就再以这个例子来探讨一下ViewGroup的onInterceptTouchEvent方法。

public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

     以上是ViewGroup的onInterceptTouchEvent的源码,我们可以理解成默认是返回false的,表示不拦截Touch事件。那么默认情况下,Touch事件是怎么传递的呢?在这里我要强调一句:View是没有onInterceptTouchEvent方法的,只有dispatchTouchEvent、onTouchEvent。

     当ViewGroup的onInterceptTouchEvent返回super或者false的时候,我们会得到以下log:


     从以上log我们可以看出,事件由ViewGroup的dispatchTouchEvent方法传递给onInterceptTouchEvent方法,再传递给子View的dispatchTouchEvent方法,最后传递给子View的onTouchEvent方法来处理。

    那如果我们将TestLayout的onInterceptTouchEvent方法的返回值改成true来拦截该事件呢?我们又会得到什么情况呢?


     我们可以发现,该事件不会传递到子View,并且会由onInterceptTouchEvent传递给onTouchEvent,由于没有任何部分能处理DOWN事件,所以MOVE和UP事件也不复存在了。这就是我们看到上述log并没有打印两次的原因。


    至此,Touch事件在ViewGroup中的传递我们基本上已经完全弄清楚了。那接下来我们看看当事件传递给子View后是怎么分发和传递的呢?首先我们也是来研究一下View的dispatchTouchEvent方法。同样还是从源码入手:

public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

       相比较ViewGroup的dispatchTouchEvent方法,View的dispatchTouchEvent方法要简便得多,主要是判断了有没有注册onTouchListener,如果onTouchListener中的onTouch方法返回true则不会走onTouchEvent方法。这也解释了我们在前面一篇所得到的onTouch方法的优先级高于onTouchEvent。从上面的log我们也能看出,当View 的dispatchTouchEvent返回super.dispatchTouchEvent时,事件是会传递给View的onTouchEvent的。那我们将该返回值改成true和false再来看看log的情况:

        先将返回值改为true。


      改成false的情况如下:


     我们可以看出,跟ViewGroup的情况非常相似,无论是返回true还是false,事件都没办法传递到View的TouchEvent中来。而这里我们会发现一个特别有意思的地方,当返回值为false的时候,事件会向上传递给父容器的onTouchEvent方法。这就是Touch事件的传递规则,先由外至内,如果内部不消化的话再由内传递至外。

    我们还是可以得出一个结论,只有当dispatchTouchEvent返回值是super.dispatchTouchEvent(ev)时,Touch事件才能向下传递。

    那最后我们再来看看View的onTouchEvent方法,默认返回值是false,代表不处理,不处理的话会传递给父控件的onTouchEvent方法。那我们如果将onTouchEvent的返回值改成true呢?

   

     我们可以看到,onTouchEvent方法返回true就将该事件消费了,事件不会再由内向外传递。那我们就通过一张流程图来总结一下Touch事件在ViewGroup和View中的分发和传递。




         通过上图,我们已经非常清楚地了解到了事件的分发机制。那最后我们就来了解一下,解决实际开发中冲突的两类方法:

         (1)、外部拦截法。所谓外部拦截法,就是Touch事件先传递给父容器,由父容器来决定拦截处理。如果父容器需要该事件,那就在onInterceptTouchEvent中返回true进行拦截,如果不需要该事件那就在onInterceptTouchEvent中返回false,将事件传递给子View。

        (2)、内部拦截法。内部拦截法顾名思义就是父控件默认不拦截任何事件,所有事件全部传递给子View,如果子View需要事件则在onTouchEvent中返回true消费,否则返回false交给父容器处理。这里需要配合requestDisallowInterceptTouchEvent方法使用,才能达到想要的效果。我们要重写子View的dispatchTouchEvent方法,根据具体情况调用requestDisallowInterceptTouchEvent来对请求父控件对事件进行拦截和不拦截。这里需要特别注意的是,父控件一定不能拦截DOWN事件,要不然所有事件都不会传递到子View了(前面已经验证了),内部拦截法也起不了作用了。



        至此,Touch事件分发机制基本上全部讲完了,相信大家结合上下两篇文章,对事件分发机制都有较为深入的了解了。后续还会带来更多高质量的博客,如果大家有什么需要深入了解的知识点,也可以再留言里跟我说,我可以根据你们的留言写相应的博客。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值