View的事件体系(三)view的事件分发机制

view事件的分发机制:view的核心知识、view的难点、view的另一大难点滑动冲突解决的理论基础。

一、了解下

安卓的view层级: 其实我们平时在xml中写的view都是通过activity的setContentView被一步步加载到window上的,事件的产生也是首先从activity按照层级往下传递,一直到我们CustomView的最内层布局view,中间传递过程有着事件分发处理。
ps:图片来源网络

在这里插入图片描述

二、 事件的传递规则

1、MotionEvent

MotionEvent这个类中对事件进行了封装,我们点击、触摸滑动屏幕所产生的事件都被封装到这个类中。

2、MotionEvent的动作类型
  • MotionEvent.ACTION_DOWN
    手指刚接触屏幕,按下去的那一瞬间产生该事件。
  • MotionEvent.ACTION_MOVE
    手指在屏幕上移动时候产生该事件
  • MotionEvent.ACTION_UP
    手指从屏幕上松开的瞬间产生该事件

我们平时操作手机(点击、触摸滑动)是无非就是这三种事件其中的组合:
1、点击事件:ACTION_DOWN -> ACTION_UP
2、触摸滑动事件:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP

3、事件分发机制方法介绍

当MotionEvent产生后,系统需要把这个具体的事件传递给具体的view这个过程就是分发过程。点击事件的分发过程由三个重要的方法共同完成。
1、public boolean dispatchTouchEvent(MotionEvent event)
2、public boolean onInterceptTouchEvent(MotionEvent ev)
3、public boolean onTouchEvent(MotionEvent ev)

  • dispatchTouchEvent
    作用:用来进行事件的分发。
    说明:每个View都有此方法,MotionEvent传递到哪个view时,哪个view的dispatchTouchEvent就会被调用。
    返回值:true表示消费事件(此view或者子view消耗,拦截事件此view消耗,不拦截传递给子view消耗),事件终止。false表示此view以及子view均不消费事件,将调用父容器的onTouchEvent方法,事件回传。返回值受当前view的onTouchEvent方法和下级view(子view)的dispatchTouchEvent的返回值影响。(参看下文伪代码理解)

    ps:如果传递一圈回传回去都没有消费事件,事件由activity消费。

  • onInterceptTouchEvent
    特别说明:此方法为View容器所有,view没有此方法。
    作用:事件拦截 。
    说明:这个方法在dispatchTouchEvent中被调用,当一个MotionEvent传递到此view容器时,首先调用此view容器的dispatchTouchEvent,在dispatchTouchEvent方法中调用onInterceptTouchEvent方法判断是否拦截事件。
    返回值:true拦截事件传递,事件不在向下分发,开始调用本view容器的onTouchEvent进行消费事件。
    false不拦截事件传递,事件开始向下传递,事件分发到子view容器的dispatchTouchEvent中。

  • onTouchEvent
    作用:对事件进行消费。
    说明:这个方法在dispatchTouchEvent进行调用。
    返回值:true表示事件被消费,本次的事件终止。false表示事件没有被消费,将调用父View的onTouchEvent方法。

4、事件分发机制三个方法关系

伪代码表示:

public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;//事件是否被消费
        if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
            consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
        }else{
            consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
        }
        return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
    }

onTouchEvent(MotionEvent ev){
   if(消费){
   
     判断,进行 消费处理
     
   }else{// 不消费时
      super.onTouchEvent(ev)
   }
}

1、可以一目了然看到:onInterceptTouchEvent,onTouchEvent都是在dispatchTouchEvent中被调用。
2、这个返回值也验证了返回值受本view的onTouchEvent,或者子view的dispatchTouchEvent影响。
3、注意这个伪代码是viewGroup的。view收到事件则走onTouchEvent判断是否消费。
4、view容器有dispatchTouchEvent 、onInterceptTouchEvent、onTouchEvent,view只有dispatchTouchEvent 、onTouchEvent。

5、事件分发机制图解

分发结构图(图片来自网络):
在这里插入图片描述

通过结构图我们知道viewGroup可以向下分发,或者不分发事件。

逻辑流程(图片来自网络)

在这里插入图片描述

6、事件传递机制一些结论

(1)同一个事件序列是指从手指触摸屏幕那一刻起,到手指离开屏幕那一刻结束,这一过程所产生的事件。这个事件以down开始,中间有不确定的move,以up结束。
(2)正常情况下一个事件序列只能被一个view拦截消耗。因为一旦一个元素拦截此事件,那么这个事件序列就会直接交给他处理,因此同一事件序列不能由两个view处理。通过特殊手段可以做到比如一个view将本该自己处理的onTouchEvent强行转给其他view。
(3)某个view一旦处理事件,如果他不消耗action down事件(onTouchEvent返回false),同一事件序列中的其他事件都不会交给他处理。事件交个他的父控件处理,父元素的onTouchEvent被调用。
(4)如果view不消耗除action down 以外的其他事件,那么点击事件会消失,父元素的onTouchEvent并不会被调用,且当前view可以持续受到后续事件,最终这些消失的点击事件会传递给activity
(5)viewGroup默认不拦截任何事件,viewGroup源码中onInterceptTouchEvent默认返回false
(6)view没有onInterceptTouchEvent,一旦有时间传递给他便走onTouchEvent方法
(7)view的onTouchEvent方法默认消耗事件返回true,除非他是不可点击的(clickable和longclickable同时为false),view的longclickable默认为false,clickable分情况,比如Button默认为true,Textview的默认为false。
(8)view的enable不影响OnTouchevent的默认返回值
(9)onClick点击事件可以发生的前提是view可点击,且他收到down和up事件。
(10)事件的传递过程是由外向内的,事件总是先传递给父元素,然后由父元素分发给子view,通过子view的requestDisallowInterceptTouchEvent可以在子view元素中干预父元素的分发过程,但是down事件除外。

三、事件分发源码解析

1、activity对事件分发过程

MotionEvent的产生,事件最先传递个activity的,由activity的dispatchTouchEvent进行分发,具体的分发工作是由activity内部的window来完成的。window会将事件传递个decorrview ,decorrview 一般就是我们setContentView所设置view的父容器。通过activity的 getWindow().getDecorView()可以获得decorview。
接下来从activity的dispatchTouchEvent开始分析

  /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
       // 事件交付给了window处理
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

事件首先交个activity所属的window进分发,如果返回true整个事件循环就结束,返回false意味着事件没人处理,所有view的onTouchEvent都返回了false,那么activity自己消费,调用onTouchEvent。

2、window的事件分发

上面我们知道activity吧事件交个了window进行分发所以我们跟踪代码进入:superDispatchTouchEvent(ev)

/**
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 *唯一的实现类是:PhoneWindow
 */
public abstract class Window 
.....


  /**
     * Used by custom windows, such as Dialog, to pass the touch screen event
     * further down the view hierarchy. Application developers should
     * not need to implement or call this.
     *
     */
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
.....

进入方法我们才知道这个方法是个抽象方法,一看window就是个抽象类,所以我们只能看他的实现类了,他的唯一实现类:PhoneWindow(源码注释有说明)所以我们就看PhoneWindow的superDispatchTouchEvent

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

通过源码轻松看出:PhoneWindow将事件分发交给了mDecor(DecorView)

1、我们可以通过: ((ViewGroup) getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);获得setContentView设置的view,这个view就是mDecor(DecorView)的子view。
2、通过activity的 getWindow().getDecorView()可以获得DecorView。
事件传递给DecorView后会传递给他的子view,也就是我们setContentView所设置的view,一般来说这个view都是容器(viewgroup)类型(参看我们平时写的xml,最外层是个容器),这时事件便在容器中开始传递了。

3、顶级view(一般为容器)对事件的分发处理(以viewGroup分析)

viewGroup的dispatchTouchEvent(MotionEvent ev)局部代码:

拦截事件时:

 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();//清除FLAG_DISALLOW_INTERCEPT设置,并且mFirstTouchTarget 设置为null
            }
 // Check for interception.
            final boolean intercepted;//默认不拦截
            // action down 和 mFirstTouchTarget != null这两种情况下才判断是否拦截
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
               //FLAG_DISALLOW_INTERCEPT是子类通过requestDisallowInterceptTouchEvent方法进行设置的
               // 判断是否子类干涉
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                //调用onInterceptTouchEvent方法判断是否需要拦截
                    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;
            }

从源码可以看出viewGroup在MotionEvent.ACTION_DOWN或者mFirstTouchTarget != null时这两种情况下会判断是否要拦截事件
ACTION_DOWN还好理解,mFirstTouchTarget != null舍意思呢?
当viewGroup不拦截事件,将事件交给子元素时mFirstTouchTarget 就指向子元素,mFirstTouchTarget 就不为空,如果viewGroup拦截事件mFirstTouchTarget 就为空了。

1、我们前面说过子View可以通过requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外)
为什么ACTION_DOWN除外?通过上述代码我们不难发现。如果事件是ACTION_DOWN,那么ViewGroup会重置FLAG_DISALLOW_INTERCEPT标志位并且将mFirstTouchTarget 设置为null。
2、当事件为ACTION_DOWN 或者 mFirstTouchTarget !=null(即事件由子View处理)时会进行拦截判断。具体规则是如果子View设置了FLAG_DISALLOW_INTERCEPT标志位,那么intercepted =false。否则调用onInterceptTouchEvent方法。
3、如果事件不为ACTION_DOWN 且事件为ViewGroup本身处理(即mFirstTouchTarget ==null)那么intercepted =false,很显然事件已经交给自己处理根本没必要再调用onInterceptTouchEvent去判断是否拦截。

小结:
当ViewGroup决定拦截事件后,后续事件将默认交给它处理并且不会再调用onInterceptTouchEvent方法来判断是否拦截。子View可以通过设置FLAG_DISALLOW_INTERCEPT标志位来不让ViewGroup拦截除ACTION_DOWN以外的事件。
所以我们知道了onInterceptTouchEvent并非每次都会被调用。如果要处理所有的点击事件那么需要选择dispatchTouchEvent方法
而FLAG_DISALLOW_INTERCEPT标志位可以帮助我们去有效的处理滑动冲突

viewGroup不拦截事件时:

     final View[] children = mChildren;
        //对子View进行遍历
        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;
            }
 
            //判断1,View可见并且没有播放动画。2,点击事件的坐标落在View的范围内
            //如果上述两个条件有一项不满足则continue继续循环下一个View
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }
 
            newTouchTarget = getTouchTarget(child);
            //如果有子View处理即newTouchTarget 不为null则跳出循环。
            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);
            //dispatchTransformedTouchEvent第三个参数child这里不为null
            //实际调用的是child的dispatchTouchEvent方法
            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();
                //当child处理了点击事件,那么会设置mFirstTouchTarget 在addTouchTarget被赋值
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                //子View处理了事件,然后就跳出了for循环
                break;
            }
        }
    }


viewGroup不拦截时事件传递给子view:
首先遍历viewgroup的所有子元素,判断view是否能够接收击事件。
两个因素:
1、View可见并且没有播放动画。
2、点击事件的坐标落在View的范围内
这两个有一个不满足就继续遍历其他view

可以看到dispatchTransformedTouchEvent实际调用的是child的dispatchTouchEvent方法,
进入dispatchTransformedTouchEvent会发现如下代码,完成了分发过程。

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

ViewGroup会遍历所有子View去寻找能够处理点击事件的子View(可见,没有播放动画,点击事件坐标落在子View内部)最终调用子View的dispatchTouchEvent方法处理事件
当子View处理了事件则mFirstTouchTarget 被赋值,并终止子View的遍历。
如果ViewGroup并没有子View或者子View处理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup会去处理这个事件(本质调用View的dispatchTouchEvent去处理)

4 、view对事件的处理
  /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        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;
            }
            
            //当前监听事件
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            // 如果设置了OnTouchListener,且onTouch返回true  ( li.mOnTouchListener.onTouch(this, event))
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            //result为false调用自己的onTouchEvent方法处理
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

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

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

1、上面代码我们可以看到view的dispatchTouchEvent中View会先判断是否设置了OnTouchListener,如果设置了OnTouchListener并且onTouch方法返回了true,那么onTouchEvent不会被调用。
2、没有设置OnTouchListener或者设置了OnTouchListener但是onTouch方法返回false则会调用View自己的onTouchEvent方法。接下来看onTouchEvent方法:

class View:
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //1.如果View是设置成不可用的(DISABLED)仍然会消费点击事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        ...
        //2.CLICKABLE 和LONG_CLICKABLE只要有一个为true就消费这个事件
        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) {
                        // 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)) {
                                    //3.在ACTION_UP方法发生时会触发performClick()方法
                                    performClickInternal();//点击进入就是performClick()
                                }
                            }
                        }
                        ...
                    break;
            }
            ...
            return true;
        }
        return false;
    }

1、可以看出即便View是disabled状态,依然不会影响事件的消费,只是它看起来不可用。
2、只要CLICKABLE和LONG_CLICKABLE有一个为true,就一定会消费这个事件,就是onTouchEvent返回true。这点也印证了我们前面说的View 的onTouchEvent 方法默认都会消费掉事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false。
3、在ACTION_UP方法发生时会触发performClick()方法(如下代码)

performClick:

  /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    // NOTE: other methods on View should not call this method directly, but performClickInternal()
    // instead, to guarantee that the autofill manager is notified when necessary (as subclasses
    // could extend this method without calling super.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;
    }

如果View设置了OnClickListener,那么会回调onClick方法。

5、补充

1、View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false。
2、注意这是默认情况,我们可以单独给View设置clickable属性,但有时候会发现View的setClickable方法失效了。(设置了clickable为false还是可以点击)
3、假如我们想让View默认不可点击,将View的clickable设置成false,我们又给View设置了OnClickListener点击事件,那么你会发现View默认依然可以点击,也就是说setClickable失效了。
原因:
View的setOnClickListener会默认将View的clickable设置成true。
View的setOnLongClickListener同样会将View的longClickable设置成true。

参看如下源码解释:

参看源码:

class View:
    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;


四、小结

看着书们参考着文章终于总结了一遍,在事件分发机制中感觉viewGroup那块最难,既然总结了一遍就加深了印象,以后多看看慢慢消化
参考文章:一文读懂Android View事件分发机制

The end

本文来自<安卓开发艺术探索>笔记总结

猜你喜欢

转载自blog.csdn.net/qq_38350635/article/details/89158550