AndroidView事件体系,事件分发机制

前记

最近重新看了开发艺术的事件分发章节,在这里记录下新的学习体会

问题记录

一个warning: onTouch lambda should call View#performClick when a click is detected.
意思说我设置了touchListener的同时还需要调用performClick方法,为什么系统硬要我调用这方法,下面我们从源码去分析这个问题
在这里插入图片描述

前置知识
MotionEvent

手指接触屏幕会产生一系列事件,而这些事件就是以MotionEvent为载体进行传递的,它有几种类型
在这里插入图片描述

重要方法
  1. dispatchTouchEvent
    用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent方法影响,表示是否消耗此事件。
    requestInterceptTouchEvent > dispatchTouchEvent

  2. onInterceptTouchEvent
    在上述方法dispatchTouchEvent内部调用,用来判断是否拦截某个事件,返回结果表示是否拦截当前事件。如果当前View拦截了某个事件,则交给onTouchEvent继续处理。并且同一个事件序列当中,此方法不会被再次调用。

  3. onTouchEvent
    同样也会在dispatchTouchEvent内部调用,用来处理Touch事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

  4. mFirstTouchTarget

分发流程

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity–>Window–>View,即事件总数先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依次类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理, 即Activity的onTouchEvent方法会被调用。这个过程其实很好理解,我们可以换一种思路,假设点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定(onTouchEvent返回了false),现在该怎么办呢?难题必须要解决,那就只能交给水平更高的上级解决(上级的onTouchEvent被调用),如果上级再搞不定,那就只能交给上级的上级去解决,就这样难题一层层地向上抛,这是公司内部一种常见的处理问题的过程。

摘自开发艺术探索

先看一段伪代码

// 父View调用dispatchTouchEvent()开始分发事件
public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    // 父View决定是否拦截事件
    if(onInterceptTouchEvent(event)){
        // 父View调用onTouchEvent(event)消费事件,如果该方法返回true,表示
        // 该View消费了该事件,后续该事件序列的事件(Down、Move、Up)将不会在传递
        // 该其他View。
        consume = onTouchEvent(event);
    }else{
        // 调用子View的dispatchTouchEvent(event)方法继续分发事件
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}
结论
  1. 同一个事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件的序列以down开始,中间含有数量不定的move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某个事件,那么同一个事件序列的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
  3. 某个View一旦决定拦截,那么这个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否拦截了。
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一件序列中的其他事件都不会再交给它处理,并且事件 将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短时间内上级就不敢再把事件交给这个程序员做了,二者是类似的道理。
  5. 如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
  6. ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
  7. View没有onInterceptTouchEvent方法,一旦点击事件传递给它,那么它的onTouchEvent方法就会被调用。
  8. View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
  9. View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
  10. onClick会发生的前提是当前View是可点击的,并且它接收到了down和up事件。
  11. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外
事件消费流程
  • 先说上面问题的原因,当事件分发到某个view时,会调用view.dispatchTouchEvent,当我们给view设置了onTouchListener时,会先用listener的onTouch,如果成功消费了此事件,则不会调用到onTouchEvent,而view的onClick方法是在onTouchEvent里面触发的,onTouchEvent都不走了,怎么会调到onClick呢,所以会有上面的警告
  • 具体调用顺序 mOnTouchListener.onTouch > onTouchEvent > view.onClick
    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)) {
                    // 如果它的touchListener不为空,先调用listener的onTouch
                result = true;
            }

            if (!result && onTouchEvent(event)) {
            		// 上面未消费成功才会走到onTouchEvent
                result = true;
            }
        }

...............省略
        return result;
    }
  • 继续看view.onTouchEvent,省略无关代码,在ACTION_UP事件里面发现了performClickInternal()这个方法,就是通过这里调用到view的onClick
 public boolean onTouchEvent(MotionEvent event) {
      ......................
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                 ......................
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            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();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        ..........................
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                  -----
                    break;

                case MotionEvent.ACTION_CANCEL:
                 ----
                    break;

                case MotionEvent.ACTION_MOVE:
                    -----
                    break;
            }
            return true;
        }
        return false;
    }
事件冲突解决

什么叫事件冲突:事件只有一个,多个对象想要处理或者处理的事件对象不是我们想给的对象
一般来说,事件冲突有两种解决方式,外部拦截和内部拦截,写法都是固定的,这里直接粘出来

外部拦截
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean isIntercept = false;
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            isIntercept=false;
            break;
        case MotionEvent.ACTION_MOVE:
           if(父容器需要此事件){
           		isIntercept =true
           }else{
           		isIntercept =false
           }   
            break;
        case MotionEvent.ACTION_UP:
            isIntercept=false;
            break;
    }
    return isIntercept;         //返回true表示拦截,返回false表示不拦截
}
内部拦截
//子view的代码·
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if () {    // 允许父View进行事件拦截,交给父View处理
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.dispatchTouchEvent(ev);
}
//父viewgroup代码     (要确保down是不拦截,move和up时要拦截)
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if(ev.getAction()==MotionEvent.ACTION_DOWN){
        return false;
    }else{
        return true;
    }
}
分析

外部拦截
外部拦截很简单,就是由父View来控制,想要事件的时候就调用onInterceptTouchEvent为true进行拦截,看下源码中是怎么进行拦截的

  • ViewGroup的onDispatchTouchEvent,disallowIntercept是通过子view调用requestDisallowIntercepteTouchEvent来进行控制的,true表示不允许父View拦截,我们这里子view未设置,默认允许拦截,然后通过onInterceptTouchEvent获取是否拦截,假设我们进行了拦截,

.............. 省略若干代码
            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;
            }
.............. 省略若干代码
  • 接着上面,当intercepted 为true是,走到了下面的if条件中,mFirstTouchTarget代表的含义就是该事件序列是否已经分发给子view了,下面的if 表示从未分发给子View过,else表示该事件序列以及分发过,这里我们外部拦截,有分发给子View,走else,通过调用dispatchTransformedTouchEvent方法,会将mFirstTouchTarget清空,还会给View发送一个ACTION_CANCER,告诉他事件结束了,然后下一次的MOTION_EVENT来的时候mFirstTouchTarget此时为null,命中if,分发事件给自己
// Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
            	// 没有分发给子View过
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                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;
                }
            }

内部拦截
内部拦截稍微复杂点,不仅要写父View还要些子View,我们上面的处理方式中,父view中ACTION_DOWN中是一定不能拦截的,因为一旦拦截了,事件就再也无法分发给子view了

// 父view中的写法
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if(ev.getAction()==MotionEvent.ACTION_DOWN){
        return false;
    }else{
        return true;
    }
}
  • 源码,还是这个判断,如果父view在ACTION_DOWN中拦截了,那么mFirstTouchTarget一定是为null的,因为没有子view可以分发下去,而事件序列中只有第一次是ACTION_DOWN,而且在ACTION_DOWN时disallowIntercept标记会被清空,也就是说子View设置的requestDisAllowInterceptTouchEvnet在ACTION_DOWN时不会生效 ,所以这里判断直接走到了else中,绕过了disallowIntercept这个参数的判断,就会导致内部拦截失效
if (actionMasked == MotionEvent.ACTION_DOWN) {
				// ACTION_DOWN 事件,清空mFirstTouchTarget 和 disallowIntercept 标记
                // 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();
            }


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;
            }
最终总结下

父View可以抢子view的事件,通过onInterceptTouchEvent方法,而一旦父View拿到了事件,子View就无法再对父View进行任何的干预,毕竟事件是从父亲发下来的,现在父亲都不给你了,你还怎么还给父亲。换句话说,一旦事件交给了某个View进行处理,接来下的事件就不会到该View的子View去了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值