为了账号安全,请及时绑定邮箱和手机立即绑定

Android事件分发机制,一篇文章就够了!

标签:
Android

说起Android事件分发,网上大大小小的文章不胜枚举,最近项目中遇到了些事件冲突的问题,发现自己对Android事件分发机制掌握的还不够好,于是最近整体学习了一波,虽然不是多么高端的技术,但作为Android知识图谱里的重要一环,必须得牢固掌握,下面这张图是前几天学习过程中记录整理的:

webp

事件分发.png

Android事件分发简单的来说就是当手指触摸屏幕之后,产生的一系列ACTON_DOWN,ACTON_MOVE(多个),ACTON_UP等事件的去向,一般我们所分析的是Action_down事件的分发过程,因为move和up事件与其基本相同,对于App开发来说,最直观的页面就是Activity,事件分发是从Activity开始一直延伸到最深处的view,形如Activity-ViewGroup-View的形式。

事件分发主要设计三个重要的方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent,就像它们的名字一样,dispatchTouchEvent是最先调用的方法,它负责事件的分发工作,onInterceptTouchEvent方法只存在于ViewGroup中(View中不存在onInterceptTouchEvent方法),表示是否拦截Event事件,onTouchEvent方法就是具体处理事件地方了。

1.Activity事件分发

下面我们就从Activity说起,当我们点击屏幕的时候,就会产生Event事件,此时会首先调用Activity的dispatchTouchEvent方法,源码如下:

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

从上面的代码我们可以看出,如果是ACTION_DOWN事件,会调用onUserInteraction()方法,onUserInteraction方法的作用是当触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法,所以可以用在屏保应用上。

紧接着调用了getWindow().superDispatchTouchEvent(ev),即调用了Window的superDispatchTouchEvent,Window是抽象类,其子类为PhoneWindow,找到此方法源码如下:

@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {      return mDecor.superDispatchTouchEvent(event);
}

调用的是mDecor的superDispatchTouchEvent方法,mDecor是DecorView类型,继续查看其源码:

public boolean superDispatchTouchEvent(MotionEvent event) {        return super.dispatchTouchEvent(event);
}

DecorView调用的是其父类,它的父类是Framelayout,Framelayout并没有重写dispatchTouchEvent方法,所以最终调用的自然是其父类ViewGroup的dispatchTouchEvent.
在分析ViewGroup事件分发之前,先给出Activty的事件分发结论:

Activty事件分发很简单,如果重写了dispatchTouchEvent方法,无论是返回值是false还是true,此时事件分发结束,即不再向下传递,只有返回super.dispatchTouchEvent()时,才会正常往下传递;

2.ViewGroup事件分发

Activity如果没有消费事件,就会传递给ViewGroup来处理,此时首先会调用ViewGroup的dispatchTouchEvent()方法,精简源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {    if (onFilterTouchEventForSecurity(ev)) {        final int action = ev.getAction();        final int actionMasked = action & MotionEvent.ACTION_MASK;
        ---> 1
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);            //会清空mFirstTouchTarget,重置FLAG_DISALLOW_INTERCEPT
            resetTouchState();
        }        //intercepted表示是否拦截事件
        final boolean intercepted;        //如果是ACTION_DOWN事件或者mFirstTouchTarget!=null,进入条件,当一条事件序列来临时,mFirstTouchTarget默认为null
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;            //判断disallowIntercept的值,此值是子view通过调用getParent().requestDisallowInterceptTouchEvent(flag)
            //来控制的,即主动要求父控件是否拦截,true表示不要拦截,false表示拦截
            //注意:requestDisallowInterceptTouchEvent控制的是下次event事件,当前的事件(一般指Action_down)是无法控制的
            if (!disallowIntercept) {                //不拦截则调用onInterceptTouchEvent方法,根据其返回值决定是否拦截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }        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) {
                    ---> 2
                    //循环遍历子view
                    for (int i = childrenCount - 1; i >= 0; i--) {                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);                        if (childWithAccessibilityFocus != null) {                            if (childWithAccessibilityFocus != child) {                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);                            continue;
                        }                        //找到child view
                        newTouchTarget = getTouchTarget(child);                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;                            break;
                        }
                        resetCancelNextUpFlag(child);                        //根据dispatchTransformedTouchEvent返回值(返回值由该子View的dispatchTouchEvent决定)判断子view是否处理该事件
                        --->3
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;                            break;
                        }
                    }
                }                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    newTouchTarget = mFirstTouchTarget;                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                }
            }
        }        //没有找到子view
        --->4
        if (mFirstTouchTarget == null) {
            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;
            }
        }        //事件cancel,up等重置一些变量
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {            final int actionIndex = ev.getActionIndex();            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }    return handled;
}

一开始判断了onFilterTouchEventForSecurity()的返回值,关于onFilterTouchEventForSecurity的作用,点击此处查看,-->1处可以看到,当ACTION_DOWN事件来临时,会进行把一些标志位清空或者恢复初始状态,mFirstTouchTarget就是在resetTouchState()方法里进行清空的,那么mFirstTouchTarget是在哪里赋值的呢,往下寻找可以发现,当找到接收Event事件的子View时 ,mFirstTouchTarget被赋值。

紧接着一个if判断,条件是actionMasked == MotionEvent.ACTION_DOWN
或者mFirstTouchTarget != null,ACTION_DOWN事件一开始肯定为true,因为最开始产生的Event事件就是它,进入判断之后,有一个disallowIntercept变量,它的值由(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0来决定,那么它又是干嘛的呢,通过搜索我们发现它是在requestDisallowInterceptTouchEvent方法中进行赋值的;

requestDisallowInterceptTouchEvent方法的作用就是子View可以控制父View是否拦截事件,子view通过调用getParent().requestDisallowInterceptTouchEvent(true/false)方法来控制父view是否拦截事件,true是不拦截,false是拦截

默认情况下是false,所以此时会调用onInterceptTouchEvent方法来进行拦截操作,true表示拦截事件,交由ViewGroup自身的onTouchEvent来处理该事件,注意,一旦onInterceptTouchEvent返回true之后,在此事件序列中,之后的所有事件不再调用onInterceptTouchEvent,而是直接交由自身onTouchEvent处理,为什么呢?看这一小段代码:

 if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
}

因为要想执行onInterceptTouchEvent方法,actionMasked == MotionEvent.ACTION_DOWN
或者mFirstTouchTarget != null必须满足其中之一,ACTION_DOWN肯定是false,因为后续事件不会再出现ACTION_DOWN事件,mFirstTouchTarget上面说了,是找到子view后才会赋值,此时还没开始找子view,父view就自己来处理事件了,所以mFirstTouchTarget此时为null,两个条件都不满足,不再继续调用onInterceptTouchEvent拦截后续事件,而是直接intercepted为true,交由自身onTouchEvent处理;

-->2处的代码表示开始循环遍历子View,寻找当前触摸的子view,找到之后,--->3处的代码通过判断dispatchTransformedTouchEvent方法的返回值来决定事件的流向,dispatchTransformedTouchEvent方法精简源码如下:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
          View child, int desiredPointerIdBits) {            final boolean handled;            if (child == null) {
                  handled = super.dispatchTouchEvent(event);
            } else {
                  handled = child.dispatchTouchEvent(event);
            }            return handled;
}

可以看到,无论找不找的到Child View,都会调用View的dispatchTouchEvent(event)方法,此时事件分发由ViewGroup传递到View,--->4处的代码表示,如果没有子View,调用dispatchTransformedTouchEvent方法时child会传递null,接着会调用 super.dispatchTouchEvent(event),走默认的事件分发逻辑,最终还是会传递到自身来处理,因为super.dispatchTouchEvent(event)默认是不处理事件的,致此,ViewGroup的事件分发分析完毕,下面就是View的事件分发了。

3.View事件分发

对于View来说,是没有拦截一说的,即没有onInterceptTouchEvent方法,当View接收到Event事件的时候,同样会先触发dispatchTouchEvent方法,dispatchTouchEvent方法精简版源码如下:

public boolean dispatchTouchEvent(MotionEvent event) {        boolean result = false;     
        if (onFilterTouchEventForSecurity(event)) {            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            ListenerInfo li = mListenerInfo;            //如果控件设置了mOnTouchListener,并且控件是ENABLED状态的,并且onTouch的返回值为true,则会拦截此事件,不再继续分发。
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }            //如果控件没有设置mOnTouchListener,返回值就由自身的onTouchEvent决定
            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;
    }

通过上面的代码可以看出,onTouch方法是先于onTouchEvent执行的,如果View设置了mOnTouchListener,并且控件是ENABLED状态的,并且onTouch的返回值为true,则会拦截此事件,不再继续分发。如果View没有设置mOnTouchListener,返回值就由自身的onTouchEvent决定,onTouchEvent返回true表示自己消费事件,否则不消费,继续传递。

我们再来看下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();        //控件是否可以点击
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;        //如果控件的状态是DISABLED的,那么直接返回可点击的状态clickable
        if ((viewFlags & ENABLED_MASK) == DISABLED) {            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;            return clickable;
        }        //控件可点击则进入判断
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {            switch (action) {                case MotionEvent.ACTION_UP:                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {                        boolean focusTaken = false;                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();                            //触发onClick事件
                            if (!focusTaken) {                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;                    break;                case MotionEvent.ACTION_DOWN:
                    ......                    //检测长按事件
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                    ......                    break;                case MotionEvent.ACTION_CANCEL:                    //进行一些重置操作
                    break;                case MotionEvent.ACTION_MOVE:                    if (!pointInView(x, y, mTouchSlop)) {                        //移除长按等回调
                        removeTapCallback();
                        removeLongPressCallback();
                    }                    break;
            }            return true;
        }        return false;
    }

首先检测View是否是clickable,以及是否禁用等逻辑,如果控件可用可点击的状态下,进入switch判断,ACTION_DOWN主要检测长按事件的触发,主要逻辑在ACTION_UP中,ACTION_UP中执行了performClick()方法,如果设置了mOnClickListener监听,则会触发onClick事件;如果控件不可点击,则直接返回false,不拦截,事件继续传递,交由;

onLongClick是在什么时候调用的呢?

其实就是在ACTIOIN_DOWN开启长按检测,执行顺序是checkForLongClick() ->performLongClick(mX, mY)->performLongClick() ->performLongClickInternal,一旦达到长按的阈值(private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500ms),就会调用如下方法:

private boolean performLongClickInternal(float x, float y) {        boolean handled = false;        final ListenerInfo li = mListenerInfo;        if (li != null && li.mOnLongClickListener != null) {            //关键代码,此处触发了onLongClick回调
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }        return handled;
    }

所以View的事件分发顺序是:dispatchTouchEvent->onTouch->onTouchEvent->on(Long)Click。

4.为什么onInterceptTouchEvent返回true可以拦截事件?

首先当onInterceptTouchEvent返回true的时候,

if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;    if (!disallowIntercept) {            //看这里
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); 
    } else {
            intercepted = false;
    }
}  else {
        intercepted = true;
}

即intercepted的值为ture,那么此时:if (!canceled && !intercepted) {}这个判断是进不去的,也就无法传递到子view了,因为这个判断里的逻辑就是循环遍历子view,进行事件分发。

继续往下看,因为并未进入if (!canceled && !intercepted) {}判断,所以此时mFirstTouchTarget为null,会执行dispatchTransformedTouchEvent方法,由于事件并未分发到子View,所以此时参数child为null:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {    final boolean handled;    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {        if (child == null) {
              handled = super.dispatchTouchEvent(event);
        } else {
              handled = child.dispatchTouchEvent(event);
        }        return handled;
    }
}

通过上面代码可以知道,当child为null的时候,会执行super.dispatchTouchEvent(event),也就是会调用自身父类的dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,而在View的dispatchTouchEvent方法中:

if (onFilterTouchEventForSecurity(event)) {    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
        result = true;
    }
    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;
    }
}

可以看到,如果控件没有设置onTouch相关监听的话,就会去调用 onTouchEvent(event)方法,这就是onInterceptTouchEvent返回true分发到onTouchEvent的逻辑,如果设置了mOnTouchListener,并且返回了true,那么onTouchEvent方法是不会调用的。

5.滑动冲突的一般解决方式

1.通过重写父类ViewGroup的onInterceptTouchEvent方法,根据具体业务需求来决定是否要对事件进行拦截,注意:ACTION_DOWN事件不要返回true,否则子view是无法接收到事件的。

2.子View通过调用getParent().requestDisallowInterceptTouchEvent(true/false)来干预父ViewGroup的onInterceptTouchEvent的事件分发过程。

6.一些重要结论

1.当onInterceptTouchEvent返回ture时,若onTouchEvent返回true,后续事件将不再经过该ViewGroup的onInterceptTouchEvent方法,直接交由该ViewGroup的onTouchEvent方法处理;若onTouchEvent方法返回false,后续事件都将交由父ViewGroup处理,不再经过该ViewGroup的onInterceptTouchEvent方法和onTouchEvent方法。

2.当onInterceptTouchEvent返回false时,事件继续向子View分发;

3.对于子View,当onTouchEvent返回true,父ViewGroup派发过来的touch事件已被该View消费,后续事件不会再向上传递给父ViewGroup,后续的touch事件都将继续传递给子View。

4.对于子View,onTouchEvent返回false,表明该View并不消费父ViewGroup传递来的down事件,而是向上传递给父ViewGroup来处理;后续的move、up等事件将不再传递给该View,直接由父ViewGroup处理掉。

5.onTouch先于onTouchEvent调用,onClick事件是在onTouchEvent中ACTION_UP中触发的。



作者:寒小枫
链接:https://www.jianshu.com/p/d964f5330618


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消