View的事件分发机制和滑动冲突解决

View的事件分发机制

概念

所谓的事件分发机制就是对MotionEvent事件的分发过程,当一个MotionEvent产生以后,系统需要把它分发给一个具体的View,这个传递的过程就是事件分发机制

重要方法

这套机制涉及到三个重要的方法:

  1. public boolean dispatchTouchEvent(MotionEvent ev)

用于进行事件的分发,返回值受当前View的onTouchEvent()方法和下级的dispatchTouchEvent()影响
2. public boolean onInterceptTouchEvent(MotionEvent ev)

在1方法的内部调用,用于判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件
3. public boolean onTouchEvent(MotionEvent ev)

也是在1方法内部调用,用来处理点击事件,返回结果标识是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前view无法再次接受到事件
用伪代码表示他们的关系是:

private boolean dispatchTouchEvent(MotionEvent ev){
        boolean result = false;
        if (onInterceptTouchEvent(ev)){
            result = onTouchEvent(ev);
        }else {
            result = child.dispatchTouchEvent(ev);
        }
        return result;
    }

事件流程

先用一张图来说明大致流程
分发流程
点击事件的传递规则:对于一个根ViewGroup,点击事件产生后,首先会传递给他,这时候就会调用他的dispatchTouchEvent方法,如果Viewgroup的onInterceptTouchEvent方法返回true表示他要拦截事件,接下来事件就会交给ViewGroup处理,调用ViewGroup的onTouchEvent方法;如果ViewGroup的onInteceptTouchEvent方法返回值为false,表示ViewGroup不拦截该事件,这时事件就传递给他的子View,接下来子View的dispatchTouchEvent方法,如此反复直到事件被最终处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被调用,如果onTouch返回false,则当前View的onTouchEvent方法会被调用,返回true则不会被调用,同时,在onTouchEvent方法中如果设置了OnClickListener,那么他的onClick方法会被调用。
由此可见处理事件时的优先级关系: onTouchListener > onTouchEvent >onClickListener

关于事件传递的机制,这里给出一些结论:

  1. 一个事件系列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列只能由一个View拦截并消耗。
  3. 某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent
    不会再被调用。
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) ,那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用。
  5. 如果View不消耗ACTION_DOWN以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理。
  6. ViewGroup默认不拦截任何事件。
  7. View没有onInterceptTouchEvent方法,一旦事件传递给它,它的onTouchEvent方法会被调用。
  8. View的onTouchEvent默认消耗事件,除非他是不可点击的( clickable和longClickable同时为false) 。View的longClickable属性默认false,clickable默认属性分情况(如TextView为false,button为true)。
  9. View的enable属性不影响onTouchEvent的默认返回值。
  10. onClick会发生的前提是当前View是可点击的,并且收到了down和up事件。
  11. 事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。

源码分析

首先看ViewGroup的dispatchTouchEvent()方法:
这里提一下,事件最先是到Activity的dispatchTouchEvent()方法->PhoneWindow->DecorView->最后才是最顶级的ViewGroup,如果事件最后全没处理,是会交给Activity处理

public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        boolean handled = false;
        // view没有被遮罩,一般都成立
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                //作为新一轮的开始,reset所有相关的状态
                resetTouchState();
            }
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
            // 之前的某次事件已经经由此ViewGroup派发给children后被处理掉了
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // 只有允许拦截才执行onInterceptTouchEvent方法
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); 
                } else {
                    intercepted = false;
                }
            } else {
                // 在这种情况下,actionMasked != ACTION_DOWN && mFirstTouchTarget == null
                // 第一次的down事件没有被此ViewGroup的children处理掉(要么是它们自己不处理,要么是ViewGroup从一开始的down事件就开始拦截),则接下来的所有事件
                // 也没它们的份,即不处理down事件的话,那表示你对后面接下来的事件也不感兴趣
                intercepted = true;
            }     

先一步步看,从上面的代码,可以看出,有两种情况下,才会去判断是否要拦截当前事件

  • actionMasked == MotionEvent.ACTION_DOWN

这个好理解,就是判断按下的事件

  • mFirstTouchTarget != null

当事件由ViewGroup的子类成功处理时,mFirstTouchTarget会被赋值指向子元素,换句话说,当ViewGroup不拦截事件,将事件交给子元素处理时,mFirstTouchTarget才不为空,这样当move和up事件到来时,由于这两个条件为false,就导致onInterceptTouchEvent()不会被调用,并且同一序列默认交由它处理
说明1:比如A包含B,B包含C和D,C处理的事件那么A的mFirstTouchTarget就是指向B,B的mFirstTouchTarget就是指向C
很重要的一个点:如果ViewGroup拦截事件,那么mFirstTouchTarget就为null

这里有个特殊情况:FLAG_DISALLOW_INTERCEPT标记位,这个标记位一般用于子View,通过requestDisallowInterceptTouchEvent()方法设置,一旦设置这个标记位,那么ViewGroup就不能拦截除了down以外的事件,这是因为每次down的时候都会重置这个标记位,导致子view中设置的这个标记位无效

if (actionMasked == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
}

这个方法里面mFirstTouchTarget也会置位null;
每一个事件开始的Down事件,ViewGroup都会询问自身的onInterceptTouchEvent是否要拦截事件
这里总结两点:

  1. onInterceptTouchEvent()方法不是每次都调用,如果想处理所有的事件,需要在dispatchTouchEvent()中去处理,只有它是每次都会调用,当然,前提是事件能到达当前的ViewGroup
  2. FLAG_DISALLOW_INTERCEPT标记位提供了一种思路,可以用来解决手势冲突

接下来就是不拦截事件,对事件进行分发

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

第一步是遍历所有的子元素,判断是否能够接受到点击事件,判断依据是两个

  1. 是否在播放动画
  2. 是否落在子元素的点击区域内

如果某个子元素满足这两点,那么事件就交给他处理

        if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }

如果子元素的dispatchTouchEvent返回为true.那么mFirstTouchTarget就会被赋值,并且跳出for循环

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

跟进去看一下mFirstTouchTarget的赋值过程

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

可以看到,其实TouchTarget是一个单链表结构,mFirstTouchTarget是否被赋值,直接影响到ViewGroup的拦截策略,如果mFirstTouchTarget为null的话,ViewGroup就会默认拦截同一事件序列的所有事件

如果遍历完所有的子元素都没有找到人处理事件,这包含两种情况

  1. 没有子元素
  2. 子元素处理得了点击事件,但是在dispatchTouchEvent()方法返回了false,这一般是因为在onTouchEvent中返回了false

在这两种情况下,ViewGroup会自己处理事件

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

这里参数传递的为null,由下面这行源码可知:

if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }

事件交由super.dispatchTouchEvent(event)去处理,也就是View的dispatchTouchEvent()方法,跟进去看一下

View对点击事件的处理

public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
            if (onFilterTouchEventForSecurity(event)) {
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //这里使用&&,前面的如果返回false,直接跳过,不会走到后面的判断
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        return result;
    }

View对事件处理过程就很简单,因为View是一个单独的元素,他没有子元素往下传递,只能自己处理
首先会判断是否设置了OnTouchListener,如果OnTouchListener的onTouch()方法返回了true,那么就不会调用onTouchEvent了,这样是方便在外部处理点击事件

接下来看view的onTouch事件
先看view处于不可用状态下,事件的处理过程

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

从上面代码看,只要view的CLICKABLE和LONG_CLICKABLE有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管它是不是不可用状态
另外还需要注意的一个很重要的点:

case MotionEvent.ACTION_UP:
    if (mPerformClick == null) {
        mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) {
        performClickInternal();
    }
break;

当up事件触发的时候,会触发PerformClick()方法,跟进去看一下

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            //调用设置的onClick()监听
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        return result;
    }

也就是说click()事件是在onTouch()里面的up去调用,如果onTouch()都没有接收到,click()更是别想了
小知识点1:View的LONG_CLICKABLE属性默认是false,而CLICKABLE和具体的view有关,例如Button默认为true,TextView默认为false,通过设置onClickListener()和setLongClickListener()会改变他们的默认属性为true,

public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

到这里,点击事件的分发机制源码差不多就算分析完了,下面介绍一下常用的滑动冲突解决方法
滑动冲突示例

解决滑动冲突的核心就是在特定的条件下,选择父布局接受事件还是子view接受事件,不管多复杂的滑动冲突,他们之间仅仅是滑动规则不同而已,下面是解决方法

  • 外部拦截法

顾名思义,就是在外部拦截,父布局需要事件,就拦截,不需要就不拦截,所以需要重写onInterceptTouchEvent()方法,下面是代码:

public boolean onInterceptTouchEvent (MotionEvent event){
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        intercepted = false;
        break;
    case MotionEvent.ACTION_MOVE:
        if (父容器需要当前事件) {
            intercepted = true;
        } else {
            intercepted = flase;
        } 
        break;
        } 
    case MotionEvent.ACTION_UP:
        intercepted = false;
        break;
    default : 
        break;
    } 
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;

针对不同冲突,只需修改父容器需要当前事件的条件即可。其他不需修改也不能修改。

ACTION_DOWN:必须返回false。因为如果返回true,后续事件都会被拦截,无法传递给子View。
ACTION_MOVE:根据需要决定是否拦截
ACTION_UP:必须返回false。如果拦截,那么子View无法接受up事件,无法完成click操作。而如果是父容器需要该事件,那么在ACTION_MOVE时已经进行了拦截,根据上一节的结论3,ACTION_UP不会经过onInterceptTouchEvent方法,直接交给父容器处理。

  • 内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗,否则就交由父容器进行处理。这种方法与Android事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。下面是伪代码:

public boolean dispatchTouchEvent ( MotionEvent event ) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction) {
    case MotionEvent.ACTION_DOWN:
        parent.requestDisallowInterceptTouchEvent(true);
        break;
    case MotionEvent.ACTION_MOVE:
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        if (父容器需要此类点击事件) {
            parent.requestDisallowInterceptTouchEvent(false);
        } 
        break;
    case MotionEvent.ACTION_UP:
        break;
    default : 
        break;
    } 
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
    }

除了子元素需要做处理外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。因此,父元素要做以下修改:

 public boolean onInterceptTouchEvent (MotionEvent event) {
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }

这样就能完成一个手势冲突,基本原则就是这,复杂的冲突也是在这个基础上去解决
下面是阅卷易一个解决手势冲突的代码

public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mX = ev.getX();
                mY = ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(ev.getX() - mX) > Math.abs(ev.getY() - mY)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    } else {
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

滑动冲突需要好好的思考,理解其基本思想,就可以很好的解决冲突
参考书籍:Android开发艺术探索

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值