Android事件分发机制

事件冲突的场景

父子控件都支持滑动的嵌套布局会导致事件冲突,比如ScrollView和ListView、RecyclerView相互嵌套等,此时控件本身的事件处理机制不能够满足我们的业务需求,所以就需要我们自己来处理事件分发的逻辑。

主要关注的方法

Activity dispatchTouchEvent onTouchEvent
ViewGroup dispatchTouchEvent onInterceptTouchEvent onTouchEvent
View dispatchTouchEvent onTouchEvent

首先我们先看事件分发的日志,通过日志分析事件分发流程。

1.所有方法都采用默认的super
TouchActivity: dispatchTouchEvent: -------eventaction down------TouchActivity
TouchLayout: dispatchTouchEvent: -----eventaction down--------TouchLayout
TouchLayout: onInterceptTouchEvent: ------eventaction down-------TouchLayout
TouchView: dispatchTouchEvent: -------eventaction down------TouchView
TouchView: onTouchEvent: -------eventaction down-----TouchView
TouchLayout: onTouchEvent: ------eventaction down-------TouchLayout
TouchActivity: onTouchEvent: -------eventaction down------TouchActivity
TouchActivity: dispatchTouchEvent: -------eventaction up------TouchActivity
TouchActivity: onTouchEvent: -------eventaction up------TouchActivity

可以看到事件分发是从Activity dispatchTouchEvent方法的down事件开始
然后调用Layout的dispatchTouchEventonInterceptTouchEvent方法
最后调用View的dispatchTouchEvent
onTouch事件则是从View的onTouchEvent->Layout的onTouchEvent 再到Activity的onTouchEvent
如果设置了OnTouchListener 则优先调用OnTouchListeneronTouch方法。
可以看到如果我们都不处理down事件,最后会交给Activity的onTouchEvent 方法处理,而后续事件会直接走Activity 的事件方法。

2.Layout onInterceptTouchEvent返回true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchLayout: onTouchEvent: ------eventaction down
TouchActivity: onTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchActivity: onTouchEvent: -------eventaction up

LayoutonInterceptTouchEvent方法返回为true时,Layout拦截了事件,可以看到在onInterceptTouchEvent方法调用之后直接调用了TouchLayoutonTouchEvent方法,没有向下分发。

3.layout onInterceptTouchEvent返回false

完整的分发流程

4.layout 的dispatchTouchEvent 为false
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchActivity: onTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchActivity: onTouchEvent: -------eventaction up

layout 的dispatchTouchEventfalse,事件会回到ActivityonTouchEvent方法中处理。

5.layout 的dispatchTouchEvent 为 true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchLayout: dispatchTouchEvent: -----eventaction up

layout 的dispatchTouchEventtrue,事件会消失,默认不处理。

6.layout 的onTouchEvent为 true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchView: dispatchTouchEvent: -------eventaction down
TouchView: onTouchEvent: -------eventaction down
TouchLayout: onTouchEvent: ------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchLayout: dispatchTouchEvent: -----eventaction up
TouchLayout: onTouchEvent: ------eventaction up

onTouchEvent消耗了事件,后续的同一事件序列会直接给当前View消耗,比如layout onTouchEvent返回true,则后续UP事件直接交给了layout处理,而不需要交给子view。

7.layout 的onTouchEvent为 false

完整的分发消费逻辑

8.View的dispatchTouchEvent 为false
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchView: dispatchTouchEvent: -------eventaction down
TouchLayout: onTouchEvent: ------eventaction down
TouchActivity: onTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchActivity: onTouchEvent: -------eventaction up

ViewdispatchTouchEventfalse 会直接跳过ViewonTouchEvent方法,调用layoutonTouchEvent

9.View的dispatchTouchEvent 为 true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchView: dispatchTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchLayout: dispatchTouchEvent: -----eventaction up
TouchLayout: onInterceptTouchEvent: ------eventaction up
TouchView: dispatchTouchEvent: -------eventaction up

ViewdispatchTouchEventtrue,代表默认消耗了事件。

10.View的onTouchEvent为 true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchView: dispatchTouchEvent: -------eventaction down
TouchView: onTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchLayout: dispatchTouchEvent: -----eventaction up
TouchLayout: onInterceptTouchEvent: ------eventaction up
TouchView: dispatchTouchEvent: -------eventaction up
TouchView: onTouchEvent: -------eventaction up

onTouchEvent消耗了事件 所以事件走到onTouchEvent就已经消费了,所以不会再调用父布局的onTouchEvent

11.View的onTouchEvent为 false

完整的事件分发机制

从源码中验证事件分发的流程

入口: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)拦截,返回true,否则调用Activity .onTouchEvent

getWindow()返回的是Window实例,而Window是抽象类型,其唯一实例为PhoneWindow

The only existing implementation of this abstract class is
android.view.PhoneWindow, which you should instantiate when needing a
Window.

追溯到PhoneWindow.superDispatchTouchEvent(ev)

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    mDecor为DecorView实例,本质上是FrameLayout,所以其调用的super其实是ViewGroup里的方法。
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

那么我们来分析下ViewGroupdispatchTouchEvent方法。

// 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;
}

以上是dispatchTouchEvent的一个片段,在Down事件下和mFirstTouchTarget 不等于空的情况下会调用,初始化intercepted字段,那么mFirstTouchTarget是什么呢?后续方法中可以看到在dispatchTransformedTouchEvent返回true也就是说子view被消耗的的情况下,mFirstTouchTarget不为空并指向该子View。所以在子view消耗事件或者down事件情况下,会访问ViewgrouponInterceptTouchEvent方法,如果不是的话则被Viewgroup拦截。也就是说,一旦View决定拦截事件,后续同一序列的事件都会由这个view处理。

大家也注意到了FLAG_DISALLOW_INTERCEPT字段,貌似可以影响事件的拦截,这个字段是通过requestDisallowInterceptTouchEvent方法设置的,一般在子view中调用。此方法一旦设置了true,那么ViewGroup无法拦截除Down以外的事件,为什么说除Down以外的事件,那是因为在代码中每次down事件初始化状态。代码如下:

// Handle an initial 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();
}

综上所述requestDisallowInterceptTouchEvent虽然可以影响事件分发,但是却影响不了down事件。还有就是onInterceptTouchEvent方法并不是每次都能调用的,所以要想每次事件都能处理,就在dispatchTouchEvent方法中处理。

下面来看看ViewGroup不处理交给子View处理的逻辑:

        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);

            // If there is a view that has accessibility focus we want it
            // to get the event first and if not handled we will perform a
            // normal dispatch. We may do a double iteration but this is
            // safer given the timeframe.
            if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                    continue;
                }
                childWithAccessibilityFocus = null;
                i = childrenCount - 1;
            }

            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            newTouchTarget = getTouchTarget(child);
            if (newTouchTarget != null) {
                // Child is already receiving touch within its bounds.
                // Give it the new pointer in addition to the ones it is handling.
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            resetCancelNextUpFlag(child);
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                // Child wants to receive touch within its bounds.
                mLastTouchDownTime = ev.getDownTime();
                if (preorderedList != null) {
                    // childIndex points into presorted list, find original index
                    for (int j = 0; j < childrenCount; j++) {
                        if (children[childIndex] == mChildren[j]) {
                            mLastTouchDownIndex = j;
                            break;
                        }
                    }
                } else {
                    mLastTouchDownIndex = childIndex;
                }
                mLastTouchDownX = ev.getX();
                mLastTouchDownY = ev.getY();
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }

            // The accessibility focus didn't handle the event, so clear
            // the flag and do a normal dispatch to all children.
            ev.setTargetAccessibilityFocus(false);
        }
        if (preorderedList != null) preorderedList.clear();
    }

上述代码中可以看到,当ViewGroup不拦截时会遍历子View,将事件传递给子View,可以看到dispatchTransformedTouchEvent就是传递给子View事件的方法。大体上可以看出如果子view的dispatchTouchEvent返回true,就是说消耗了事件那么会对mFirstTouchTarget进行赋值,并中断循环。如果返回为false,那么会继续将事件分发给下一个子元素。

mFirstTouchTarget真正赋值是在addTouchTarget方法中,可以看到mFirstTouchTarget其实就是单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为空,默认拦截同一序列的所有事件,这一点在最开始就已经分析了。

 private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

当所有子元素都没有被合适的处理时,即viewGroup没有子元素,或者子元素处理了事件但是onTouch返回为false。这两种情况下ViewGroup自己处理事件。

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
  // No touch targets so treat this as an ordinary view.
   handled = dispatchTransformedTouchEvent(ev, canceled,  null,TouchTarget.ALL_POINTER_IDS);
 } 

下面是dispatchTransformedTouchEvent方法的代码:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled; 
        ...

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

我们可以看到子view为空的情况下调用super.dispatchTouchEvent,也就是View.dispatchTouchEvent。子View不为空的情况下调用子View的dispatchTouchEvent。从而完成分发的步骤。

view对事件的处理

下面我们看一下View.dispatchTouchEvent的代码

public boolean dispatchTouchEvent(MotionEvent event) {
        ....
        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;
            }
        }
       .....
    }

onFilterTouchEventForSecurity此过滤触摸事件策略是判断屏幕是否被遮挡。如果是enable状态,并且通过鼠标输入处理滚动条拖动的话result返回true。如果TouchListener不为空,View是enable状态并且TouchListener.OnTouch返回为True的话,result为true。
最后如果result为false并且onTouchEvent(event)返回为true,result为true。
这边可以得出结论,TouchListener.OnTouch的优先级高于onTouchEvent(),并且如果TouchListener.OnTouch返回为true消耗了事件,onTouchEvent不会再触发。

而在onTouchEvent方法中,UP事件处理过程中会调用performClickInternal方法,此方法最终调用performClick方法:

public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

上述代码是看是否有mOnClickListener,有的话调用OnClickListener的onClick方法。可以看到我们的onClick方法是在onTouch方法中,处于事件的最底层。自此事件分发的过程全部梳理完成。

经典案例

现如今控件功能越来越完善,以前的一些滑动异常经典案例在现在的控件上面竟然大多复现不了,真是有点小失望呢!
笔者选取ScrollView嵌套ScrollView的案例,业务需求为:

1.外层未滑动到底部,内层不允许滑动。
2.内层未滑动到顶部,外层不允许滑动。

依据业务逻辑可采用两种方式拦截事件,一种是外部拦截,另一种是内部拦截
依据之前的源码分析有两个条件控制是否拦截事件,分别是onInterceptTouchEvent返回和FLAG_DISALLOW_INTERCEPT字段的赋值,而外部拦截法就是通过控制onInterceptTouchEvent返回来控制事件的分发。内部拦截则是子view控制FLAG_DISALLOW_INTERCEPT字段的值来控制事件的分发。

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;
}
外部拦截法的基本代码:
   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int y = (int) ev.getY();

        switch(ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            case MotionEvent.ACTION_MOVE:

                if(canIntercept(y)) {// 根据冲突的不同情况自己判断
                    intercepted = true;
                }
                else {
                    intercepted = false;
                }
                break;
            default:
                break;
        }
        mLastY = y; // 用于判断是否拦截的条件
        return intercepted;
    }

    private boolean canIntercept(int y) {
        View view = findViewById(R.id.recycleView);
        if (y - mLastY > 0){
            return !view.canScrollVertically(-1);
        } else {
            return canScrollVertically(1);
        }
    }

基本流程如上,其中canIntercept方法的返回是是否拦截的条件
笔者发现一个奇怪的现象,像上述代码down事件未拦截的仅拦截的是move事件,那么会后续move事件走到了onTouchEvent方法,但是ScrollView自身的滚动事件失效了,导致整体不能滑动,这块不知道是什么原因?笔者处理方式是在onTouchEvent方法中自己处理滑动事件和fling事件来达到滚动的效果。不知道还有没有比较好的处理方式,希望有大神能够指点一下谢谢。

内部拦截法的基本代码:
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (null != mParentRecycleView) {
                    mParentRecycleView.requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (isIntercept()){
                    if (null != mParentRecycleView ) {
                        mParentRecycleView.requestDisallowInterceptTouchEvent(false);
                    }
                    return false;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (null != mParentRecycleView) {
                    mParentRecycleView.requestDisallowInterceptTouchEvent(false);
                }
                break;
        }
        return super.dispatchTouchEvent(event);
    }

大体代码如下isIntercept是父布局拦截的条件,可自行定义规则。内部拦截法需要父布局
onInterceptTouchEvent down事件返回为false也就是说父布局不拦截down事件才能生效,否则事件分发不到子布局的dispatchTouchEvent方法中。
相对于外部拦截的方式,笔者更喜欢内部拦截。因为我们的子控件有可能不仅仅只有一个有可能有多个,而如果在外层一次性处理那么多的冲突似乎有点不合适。这时候使用内部处理的话就优雅很多了,我们只需要在各自的控件中针对外层控件做下处理就行了,并不影响外层控件的代码。

总结

事件分发流程图流程图
事件分发流程图.png
梳理

1.同一事件序列指的是从手机接触屏幕的那一刻起,到手指离开屏幕那一刻结束。整体事件流程从down -> move ...move ->up
2.同一事件序列一次只能被一个view消耗,因为一旦view拦截了事件,同一序列的后续事件都会直接给他处理。
3.某个view一旦决定拦截,那么这一事件序列都只能交给他处理。从源码上分析就是一旦拦截了那么mFirstTouchView就不会赋值了,所以不会走分发事件直接交给了super.dispatchTouchEvent方法从而交给了onTouchEvent处理。
4.如果view的onTouchEvent不消耗Down事件,那么同一序列的其他事件都不会交给他处理,从业逐层询问父布局。如果都不处理,最终交给Activity的onTouchEvent。
5.View没有onInterceptTouchEvent方法,一旦事件交给他,onTouchEvent就会被调用。
6.requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,down事件除外,因为down事件会重置状态。此方法是内部拦截方法核心。
7.ViewonTouchEvent默认消耗事件,除非clickablelongClickable同时为false。ViewlongClickable默认为false,而clickable分情况,比如Button是true,TextView是false。同时enable不影响onTouchEvent的返回值。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容