Android源码角度分析事件分发消费(彻底整明白Android事件)

1,315 阅读9分钟

引言

Android 事件分发网上有很多资料,大部分都是在dispatchTouchEvent() onInterceptTouchEvent() onTouchEvent()三个方法中打印Log日志,草草的得出,各个方法中返回true/false会调用哪个方法,结论良莠不齐。没有从根本上理解这一块的实现机制,实际运用的时候 还是一阵懵逼。从源码分析,吃透其中的代码逻辑才能灵活运用解决实际问题,废话就讲这么多,进入主题。

View中的事件分发

系统默认情况下,View作为事件分发消费的终点,我们就先看下源码里view在接受到事件时候怎么处理的,view中跟事件分发相关的主要是两个方法,一个是dispatchTouchEvent(),一个是onTouch(),先看View中dispatchTouchEvent()怎么处理的;

dispatchTouchEvent()、onTouchEvent()返回值作用
  • 1、public boolean dispatchTouchEvent():默认情况下,这个方法就是把事件分发给目标view,而且这个目标view可能是自己;如果返回true就表示找到了要消费事件的目标view,而且事件被消费了 如果返回false就表示没有找到
  • 2、onTouchEvent():这个方法是处理消费触摸事件的 如果返回true表示这个事件被消费了 如果返回false 表示没有被消费
  • 3、注意 手指触碰屏幕一个完整的触碰动作包含最重要的三个事件:按下、滑动和抬起。View(ViewGroup)在做事件分发的时候,最开始接受到的是按下,当按下(ACTION_DOWN)如果没有分发成功也就是没有找到要消费的这个事件的view时,把整个触碰事件的ACTION_DOW以后的ACTION_MOVE和ACTION_UP就不会做分发了

事不宜迟上代码,关键的地方加了我骚气的中文注释

View中的dispatchTouchEvent()方法

public boolean dispatchTouchEvent(MotionEvent event) {

        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;
            }
//Flag1:如果设置了OnTouchListener监听且onTouchListener监听中的onTouch()方法为true把result设置为true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
//Flag2: 如果上一步判断的结果为false,则进入执行view自身的onTouchEvent(MotionEvent e)方法,
        将result设置为true 如果onTouchEvent(e)方法返回true
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

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


        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

认阅读上面这段代码,可以知道;

  • 通常情况下当我们给view设置了OnTouchListener也就是触摸监听的时候会先执行我们的触摸监听中的onTouch()方法
  • 并且当onTouch()方法返回false的时候才去执行view的onTouchEvent(),也就是说当onTouch()方法返回true的时候就onTouchEvent方法就不执行了;

View中的onTouchEvent()

简单分了View的onDispatchTouchEvent方法之后,我们看你下View的onTouchEvent方法

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
//Flag3: disabled view 并且是clickable的情况下依然会消费这个事件,只是不做处理。
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
//Flag4
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {

                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
//Flag4:
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }


                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }
//Flag5
            return true;
        }

        return false;
    }

关键点:

  • 敲黑白注意Flag4和Flag6处,当view为可点击的状态下,View直接会返回true
  • Flag6处 当为手势为抬起的时候会发生点击事件,结果View的dispatch方法我们知道,如果View设置了OnTouchListener并且在ontouch()中返回了true view的点击监听事件就会失效
  • 这时候再看Flag3处当View为DISABLED的状态下,如果View是CLICKABLE或者LONGCLICKABLE 就直接消费返回true ,也就是说当View为DISABLED状态下设置的OnClickListener点击监听会失效
  • 特别说明:当onTouchEvent返回true的时候就意味着,事件的分发找到了要消费他的地方,也就是本View,所以按下以后的滑动抬起这些动作都统统交到这里来消费;如果返回false就表示本View大人不消费你,以后就不要来送事件了。

ViewGroup中的事件分发消费

ViewGroup中的dispatchTouchEvent()

代码比较长,方便查看里面的逻辑,做了适当的删除,关键地方加了注释

(代码来源android-24)

 public boolean dispatchTouchEvent(MotionEvent ev) {

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {

// 首先判断是否对事件进行拦截 ,也就是给intercepted赋值
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//如果允许拦截,就交给intercepted()判断
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
//如果不允许拦截,设置为false
                } else {
                    intercepted = false;
                }
            } else {
// 如果ev不是ACTION_DOWN类型而且也没有找到消费目标 就直接返回true
                intercepted = true;
            }

            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
//如果没有拦截事件是,就去找要消费这个事件的view,并把它放在newTouchTarget中
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                            newTouchTarget = getTouchTarget(child);
//找了的事件分发的目标 跳出循环
                            if (newTouchTarget != null) {                    
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            } 
//事件分发给child,如果child消费,并把newTouchTarget赋值给,mFirstTouchTarget跳出循环
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }                         
                        }             
                    }
//
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                    }
                }
            }

//如果没有找到消费目标,就把该ViewGroup当做一个View处理,因为ViewGroup是View的子类,
//也就是说执行super.dispatchTouchEvent();也就是View的dispatchTouchEvent();
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
 //把事件分发给已经找到的消费目标
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
        return handled;
    }

关键点

  • disallowIntercept 这个参数默认是false 表示允许拦截事件,若果为true表示不需要拦截拦截事件, 可以通过requestDisallowInterceptTouchEvent()进行设置,具体应用:ViewGroup的子view通过调用这个方法可以告诉他说大哥这个事件别拦截了交给我做吧
  • 是否拦截这个决定权交给ViewGroup自己的onInterceptTouchEvent()做判断
  • 如果拦截了就直接交给 super.dispatchTouchEvent(event)进行消费
  • 如果没有拦截就找到自己要"符合条件"的子View进行事件的分发,如果事件没有分发出去也就是自己view的dispatchTouchEvent()返回了false 或者没有找到子View做也会调用super.dispatchTouchEvent()处理
  • 画图

ViewGroup中的InterceptTouchEvent()

pubic boolean onInterceptTouchEvent(){
    return false;
}

默认情况下 返回false

Activity中的事件拦截和分发

  • Q1:严格的讲,Activity不是View当然也不是ViewGroup,他的事件分发是消费怎么执行的呢?
  • Q2:为什么ac没有onInterceptTouchEvent()?

    首先第一个问题:源码中找答案

Activity中dispatchTouchEvent():
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
PhoneWindow中superDispatchTouchEvent()
   public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
DecorView
   private final class DecorView extends FrameLayout{
         public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
   }

可见Activity的本质使用的View的dispatchTouchEvent();第二个问题迎刃而解View的dispatchTouchEvent()不需要onInterceptTouchEvent().

应用

经常遇到的问题

  • 为什么只接受到了ACTION_DOWN 后续事件接收不到
    示例代码

//自定义ViewGroup
class MyViewGroup extends ViewGroup{
    public boolean dispatchTouchEvent(MotionEvent e){
    Log.d("李不凡","MyViewGroup   --- dispatchTouchEvent --- "+e..getAction());
        return  super.dispatchTouchEvent(e);
    }
    public boolean onInterceptTouchEvent(MotionEvent e){
      Log.d("李不凡","MyViewGroup   --- onInterceptTouchEvent --- "+e..getAction());
        retrun super.onInterceptTouchEvent(e)
    }
    public boolean onTouchEvent(MotionEvent e){
      Log.d("李不凡","MyViewGroup   --- dispatchTouchEvent --- "+e..getAction());
        retrun super.onTouchEvent(e)
    }

}

//自定义View
class MyViewGroup extends TextView{
    public boolean dispatchTouchEvent(MotionEvent e){
        return  super.dispatchTouchEvent(e);
    }
 public boolean onTouchEvent(MotionEvent e){
        retrun super.onTouchEvent(e)
    }

}

//xml布局
<MyViewGroup
       android:background="#ffffff"
       android:layout_width="match_parent"
       android:layout_height="300dp">
            <MyView
                //--android:clickable="true" 
                android:background="#ff0000"
                android:text="MyButton"
                android:textSize="20sp"
                android:textColor="#00ff00"
                android:id="@+id/my_view"
                android:layout_width="match_parent"
                android:layout_height="100dp" />
</MyViewGroup>
现象

运行上面这段代码会发现,我们做个点击事件,查看日志,在ViewGroup 和View中只接受到了ACTION_DOWN 而没有接收到后续的事件

源码

从上面的源码分析我们知道 默认情况下 事件分发过程的如果ViewGroup没有拦截(也就是onInterceptTouchEvent()返回值为false)就会交给子view去做分发,执行ViewGrop的返回值就交给子view的dispatchTouchEvent()的返回值决定 ,默认情况下,view中的onTouchEvent()返回值为false ,导致ViewGroup交给自己的onTouchEvent去做处理默认也是false。最终的结果就是viewgroup的dispatch返回值为false,这个结果就说明viewgroup自己的和子view都不消费这个事件 所以后续的ACTION_MOVE 、ACTION_UP等事件都接收不到

解决
    1. MyView中重写onTouchEvent()方法返回true。如下

      public boolean onTouchEvent(MotionEvent e){
      //super.ononTouchEvent(e)
         return true;
      }

      注意:如果这样操作会虽然达到了接收完整事件的目的但MyView身上设置的点击事件会失效。why?提示:屏蔽了super.ononTouchEvent(e);这行代码导致的后果

    1. 把MyView设置成onclickable可点击。源码依据如下:
   //View中onTouchEvent源码(有化简)
    public boolean onTouchEvent(){
       if ((viewFlags & ENABLED_MASK) == DISABLED) {

                return (((viewFlags & CLICKABLE) == CLICKABLE
                        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
            }

       if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClick();
                }
                break;
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            }
        return true;
    }
    retrun false;

需要注意的是:如果我们的View为DISABLED且ONCLICKABLE的时候事件也会被消费但不会做处理

    1. 给MyView设置OnTouchListener监听并在onTouch()方法中返回true,依据参照上面的View中dispatchTouchEvent()分析,这种情况也会导致onTouchEvent不会执行,所以如果给view设置点击监听也会失效。

下集预告

Android源码角度分析事件分发消费之应用篇

  • 怎么解决事件冲突
  • 自定义一个简单的Viewpager
  • RefreshSwipLayout怎么进行事件消费和分发的