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

我们的手指在屏幕上的一系列操作都会最终被系统封装到MotionEvent这个类中。事件的分发就是MotionEvent分发的过程。

一、了解下setContextView的view是如何显示的

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

在这里插入图片描述

二、 事件的传递规则

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

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

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

  • onInterceptTouchEvent

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

  • onTouchEvent

1、作用:对事件进行消费。
2、说明:这个方法在dispatchTouchEvent进行调用。ViewGroup中没有此方法的实现。具体实现在他的父类View中
3、返回值: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。故当点击事件在view的范围内时Button消耗事件,Textview不消耗。

(8)view的enable不影响OnTouchevent的默认返回值
(9)onClick点击事件可以发生的前提是view可点击,且他收到down和up事件。
(10)事件的传递过程是由外向内的,事件总是先传递给父元素,然后由父元素分发给子view,通过子view的requestDisallowInterceptTouchEvent可以在子view元素中干预父元素的分发过程,但是down事件除外。

三、事件分发源码解析

1、activity对事件分发过程

1、MotionEvent的产生,事件最先传递给activity的,由activity的dispatchTouchEvent进行分发。
2、activity会把具体的分发流程交付给window来完成。
3、window(PhoneWindow)会将事件传递给decorrview 。

decorrview 一般就是我们setContentView所设置view的父容器。
通过activity的 getWindow().getDecorView()可以获得decorview。

4、decorrview继承Framelayout(没有dispatchTouchEvent方法),Framelayout继承ViewGroup
5、最终事件在ViewGroup的dispatchTouchEvent进行分发
6、从我们setContentView设置的顶层容器开始往下分发

(1)activity#dispatchTouchEvent

    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 为ACTION_DOWN事件时
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();//查看源码发现这个方法是空实现。
        }
       // 事件交付给了window处理
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

1、首先判断事件是否为ACTION_DOWN事件,是的话调用onUserInteraction方法,其实onUserInteraction方法内部为空实现,其实我们可以重写Activity的这个方法,用于监听事件的开始,一般也没人做。
2、activity会把事件交付给getWindow处理,如果getWindow().superDispatchTouchEvent(ev)返回true整个事件循环就结束,返回false意味着事件没人处理,所有view的onTouchEvent都返回了false,那么activity自己消费,调用自己的onTouchEvent。

(2)activity#onTouchEvent

/**
     * Called when a touch screen event was not handled by any of the views
     * under it.  This is most useful to process touch events that happen
     * outside of your window bounds, where there is no view to receive it.
     *
     * @param event The touch screen event being processed.
     *
     * @return Return true if you have consumed the event, false if you haven't.
     * The default implementation always returns false.
     */
    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }

1、调用window对象的shouldCloseOnTouch方法,方法返回true时会关闭activity并使activity的onTouchEvent返回true,表示消耗了事件

(3)window#shouldCloseOnTouch

  public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        final boolean isOutside =
                event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
                || event.getAction() == MotionEvent.ACTION_OUTSIDE;
        if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
            return true;
        }
        return false;
    }
------------------------------------------------------------------------------------------
@Override
    public final View peekDecorView() {
        return mDecor;
    }

1、看下整体结构根据if判断决定返回true或者false
2、mCloseOnTouchOutside:这个变量的布尔值表达含义:

true:当前activity支持点击空白区域让activity消失,一般当activity以Dialog形式呈现时才会为true

3、peekDecorView方法:为Window中的抽象方法,具体去phoneWindow中去看。peekDecorView方法不为null则代表当前的phoneWindow持有decorview 对象。
4、isOutside:这个变量的布尔值表达含义

true:代表当前的触摸点击事件在 decorview的范围之外,事件没有点击到decorview里。

ps:三者都为true时方法返回true。

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、viewGroup对事件分发处理

1、事件传递到DecorView的dispatchTouchEvent时内部会调用父类(Framelayout->ViewGroup)dispatchTouchEvent
2、以下是viewGroup的dispatchTouchEvent(MotionEvent ev)局部代码:

(1)概括viewGroup的dispatchTouchEvent主要做的三件事

1、判断是否拦截事件
2、在当前viewGroup中找到真正点击的view
3、不拦截时事件分发到view上。

(2)ViewGroup#dispatchTouchEvent
在这里插入图片描述

1、mInputEventConsistencyVerifier

1、这里首先做了调试对象判定,
2、这个对象和整个事件分发机制没啥关系。

2、setTargetAccessibilityFocus:安卓中提供的辅助功能的选项,用来帮助有障碍的用户来使用这个系统。这个也是和整个事件分发机制没啥关系。
3、 boolean handled = false这里正式进入事件的分发机制,可以看到这个handled的值就是整个方法的返回值。

(3)ViewGroup事件分发的主要逻辑
在这里插入图片描述

 boolean handled = false;
         //1、安全策略判断
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
           // 2、判断是否是ACTION_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();
            }

            // 定义个boolean变量,其值代表是否拦截事件。
            final boolean intercepted;
            // 3、这个if else 就是判断事件是否拦截的主要代码
            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;
            }

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {

                
                //childWithAccessibilityFocus : 接收事件目标view
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
                 // 点击、移动事件判断
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
                            
                    // 清空之前所有触摸点信息
                    removePointersFromTouchTargets(idBitsToAssign);
                    // childrenCount :获得viewgroup的子view数量
                    final int childrenCount = mChildrenCount;
                    //newTouchTarget  上面有定义,初始化为null
                    if (newTouchTarget == null && childrenCount != 0) {
                        // 获得触摸点位置(x,y)坐标
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // preorderedList :获得所有能够接收事件的子view集合
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        //customOrder : 是否自定义了view的绘制顺序,为true代表用户自定义了view的绘制顺序
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //遍历子view,查找真正点击的,需要响应事件的view
                        for (int i = childrenCount - 1; i >= 0; i--) {
                           //getAndVerifyPreorderedIndex:获取子view真正索引
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            // getAndVerifyPreorderedView 获取到真正的view        
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                         
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
                             // 检查view是否能够接收触摸事件,触摸事件是否在view的范围内
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            // 获得目标view的触摸事件,newTouchTarget 不为空代表当前的子view已经获得触摸事件
                            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 检查该view是否设置了暂时不接受事件的标志位,设置了就清除掉标志位。下一次就可以接受了。
                            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();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

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

            // Update list of touch targets for pointer up or cancel, if needed.
            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);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;

1、onFilterTouchEventForSecurity(ev)首先是安全策略的判断。

1、这个方法的具体实现在View中。判断当前的触摸事件是否符合安全策略。
2、安全策略:常理来说用户只能点击自己能够直接看到的View或者ViewGroup,看不到的是不会去点击的。所以google为事件的分发制定了一个安全策略。如果当前view不在视图的顶部(被其他view遮挡)、或者当前的view设置了不相应事件,则onFilterTouchEventForSecurity方法返回false。
3、这个安全策略过滤的方法如果直接返回false的话那么ViewGroup的dispatchTouchEvent方法就直接返回false来了

2、判断是否是ACTION_DOWN事件,做一些必要初始化操作

1、cancelAndClearTouchTargets(ev):取消并清除所有触摸目标。
2、resetTouchState():重置触摸的状态,为新的时间循环做准备。

3、final boolean intercepted;语句下的fi else块就是判断事件是否被viewGroup拦截的核心实现

1、为当前事件action down事件,且mFirstTouchTarget不为空时,进入判断
2、mFirstTouchTarget不为空,代表当前存在处理事件的子view
3、disallowIntercept代表是否允许拦截事件

true:不允许拦截事件,走下面else
false:有可能拦截事件,调用onInterceptTouchEvent对intercepted进行赋值。
ps:注意这里的有可能,一般情况下onInterceptTouchEvent都会返回false。参看如下onInterceptTouchEvent的源码解析就一目了然。

4、当前不为action down或者没有子view处理事件(mFirstTouchTarget为空)这时viewgroup拦截事件。
5、事件是否拦截小结:
为action或者mFirstTouchTarget不为空,一般情况下不拦截事件。不为actiondown,mFirstTouchTarget为空viewGroup拦截事件。至此view group检测是否拦截的逻辑完毕

4、事件进行分发到指定的view或者viewGroup上

1、final boolean canceled = resetCancelNextUpFlag(this):canceled 是否为取消事件
2、boolean split= (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0:split当前事件是否分发给子视图,同一容器中可能存在多个子视图重叠在一起,这时split为true代表重叠区域子视图都可获得事件。
3、当canceled 不为取消事件,且viewGroup不拦截事件,这时事件开始像子view分发(if (!canceled && !intercepted))

1、childWithAccessibilityFocus:记录接收该事件目标view
2、接下来对事件进行判断
3、 TouchTarget newTouchTarget = null;if (newTouchTarget == null && childrenCount != 0)
4、查找真正的响应事件的view,检测是否可以接收触摸事件,检测触摸事件的范围。

5、子类中分发事件(dispatchTransformedTouchEvent):返回值为true代表子view中处理了事件

dispatchTransformedTouchEvent内的:addTouchTarget:这个方法就是为前面的mFirstTouchTarget,进行赋值

6、ziview的查找结束清理子view集合:if (preorderedList != null) preorderedList.clear();至此当前事件不是取消事件,当前的viewGroup不拦截事件处理完毕.
7、接下来dispatchTouchEvent还未结束判断mFirstTouchTarget是否为空来判定有子view处理还是交付给viewGroup的父类处理。

(4)ViewGroup#onInterceptTouchEvent

  public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

1、方法返回true,代表拦截事件,方法为false代表不拦截事件。
2、四个条件都满足情况下这个方法才返回true

1、ev.isFromSource(InputDevice.SOURCE_MOUSE):这个方法判断给定当前的事件是否来自给定的事件源。而这里传入的常量代表输入的设备为鼠标。一般情况下我们使用手指,手机使用鼠标几乎很少见。所以这里一般为false。
2、down事件判断
3、isButtonPressed(MotionEvent.BUTTON_PRIMARY):判断当前是否按下鼠标左键。手机一般都不用鼠标。
4、isOnScrollbarThumb(ev.getX(), ev.getY()):判断当前手指或者鼠标是否在滚动条上,是就返回true。

综上:一般情况下viewGroup的onInterceptTouchEvent可以认为默认返回false,默认不拦截事件。

(5)Viewgroup#dispatchTransformedTouchEvent

  /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        final int oldAction = event.getAction();
        // 1、判断是否为down事件
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            // 判断是否有子view,没有子view时,直接调用viewGroup父类的dispatchTouchEvent方法(ViewGroup父类就是View)
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
            // 存在子view时,事件交付子view的dispatchTouchEvent
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        // 获得新旧指针位
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }

        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        // 2、新旧指针位对比  纠正偏移量
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

      // 3、前两个判断都不满足时,这里判断当前的子view是否存在
        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;
    }

分析:代码虽长但是逻辑明确,主要进行了3个判断(参看源码注释)

拦截事件时:

 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那块最难,既然总结了一遍就加深了印象,以后多看看慢慢消化
下一篇:
View的事件体系(四)view滑动冲突

参考文章:
一文读懂Android View事件分发机制

ActionMasked

参考

参考

The end

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

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值