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

来源:互联网 发布:淘宝旺旺客服 编辑:程序博客网 时间:2024/05/21 08:52

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

2 0
原创粉丝点击