二. View的事件分发机制

329 阅读6分钟

不忘初心 砥砺前行, Tomorrow Is Another Day !

相关文章

本文概要:

  1. 坐标体系.
  2. View的触摸事件.

一. 坐标体系

横轴:方向从左至右,数值依次增大. 纵轴:方向从上至下,数值依次增大.


View与MotionEvent的坐标体系-图片来自《进阶之光》

1.1 对于View的坐标

  • getLeft/getTop/getBottom/getRight:都是获取四边相对于父容器的坐标,对应View的left、top、bottom、right四个参数.

left、top、bottom、right与x,y,transLationX,transLationY之间的关系

  • x = left + transLationX
  • y = top + transLationY

其中x,y代表View左上角的坐标,transLationX,transLationY代表View左上角的坐标偏移量.需要明确的是这四个参数都是相对于父容器坐标是一种相对坐标.

当在做平移的过程中,left与top的原始信息不会发生改变,改变的是x,y,transLationX,transLationY的值.

1.2 对于MotionEvent的坐标

  • getX/getY: 获取相对于当前View左上角的坐标.
  • getRawx/getRawY: 获取相对于手机屏幕左上角的坐标.

二. View的触摸事件

对于触摸事件需要明白几点.

  1. 一个事件流,是指以一个DOWN为开始,最终以UP为结束.
    • 另外一种以CANCEL为结束特殊情况,就是当ViewGroup拦截某个事件时,它会对子view发出CANCEL事件.
  2. 关键是对DOWN事件的处理,相当于是一个试探性事件,如果某个View不处理,那么其余该事件流的事件序列都不会再交给它.
  3. 可以通过 requestDisallowInterceptTouchEvent:让ViewGroup不要拦截事件,前提是ViewGroup不拦截DOWN事件.

2.1 触摸事件整体流程

主要涉及到下面三个方法.

  • dispatchTouchEvent: 分发事件
    • 返回true表示事件已经消耗.
  • onInterceptTouchEvent:拦截事件,ViewGroup仅有的方法.
    • 返回true表示拦截.
  • onTouchEvent:处理事件
    • 返回true表示已处理

整体流程具体参考下面伪代码,个人觉得将触摸事件流程体现的淋淋尽致.

/*
 * 经典伪代码
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean comsume;
        //ViewGroup是否拦截事件
        if (onInterceptTouchEvent(ev)) {//true拦截,自己处理
            if(!OnTouchListener.onTouch()){//根据OnTouchListener的OnTouch决定是否调用.
                comsume = onTouchEvent(ev);
            }
        } else {//不拦截,交给子View处理
            comsume = child.dispatchTouchEvent(ev);
        }
    }

对于上面伪代码我们来对整体流程做个总结

  • 当点击事件达到顶级View(也就是ViewGroup时),会调用ViewGroup的dispatchTouchEvent方法,在此方法中如果onInterceptTouchEvent.
    • True,则表示拦截事件,所有事件都交由它处理,具体处理流程见下一点.
    • False,则表示交由其子View去处理,即调用子view的dispatchTouchEvent.
  • 如此反复直到事件终止处理

具体处理流程

  • 在处理事件时,如果设置OnTouchListener,那么会调用onTouch方法.
    • True,则不会回调onTouchEvent.
    • false,则会回调onTouchEvent.
      • 在onTouchEvent方法中,如果设置了OnClickListener,那么OnClick方法会调用.

如果到这里,上面介绍的触摸事件内容都能很清晰的理解,那么暂时不用看下面原理分析内容,可以自己试着去分析View的事件分发机制的源码,会有不一样的收获与感受.


2.2 原理分析

Activity的分发过程

  • Activity → Window(PhoneWindow) → View(DecorView)

事件由Activity调用dispatchTouchEvent首先传入到Window,而Window的唯一实现类是PhoneWindow,最终传递到顶级View(DecorView),一直往下传递.如果这些事件都没有处理,最终又会交给Activity的onTouchEvent进行处理.

对应源码

//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //调用window的superDispatchTouchEvent
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
//PhoneWindow.java 
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //调用DecorView的superDispatchTouchEvent
        return mDecor.superDispatchTouchEvent(event);
    }

//DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
        //调用ViewGroup的dispatchTouchEvent
        return super.dispatchTouchEvent(event);
    }

ViewGroup的分发过程

ViewGroup的dispatchTouchEvent分发过程比较复杂分层2个部分去看.

1. 对拦截事件的处理

对应源码

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //...省略部分代码
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            /*
             *  1.当是ACTION_DOWN事件时,则重置状态
             */
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            /*
               2.检查是否拦截
                mFirstTouchTarget: 记录消耗事件的子view
             */
            
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//DOWN事件或者有子View消耗了事件
                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 {
                intercepted = true;
            }

         
2. 寻找能接受事件的子view.

对应源码

        if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        //点击位置多个子View重叠问题处理
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        /*
                          1. 遍历所有子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;
                            }

                            /*
                              2. 寻找是否能接受点击事件
                              canViewReceivePointerEvents:是否在播放动画
                              isTransformedTouchPointInView:点击事件的坐标是否落在子元素区域内
                             */
                            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);
                            /*
                              3.事件传递给子View处理,
                            */
                            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();
                                //3.1 true,如果处理了事件,则赋值mFirstTouchTarget,跳出循环.
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            
                           
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    
                    
            if (mFirstTouchTarget == null) { //3.2 无子view或者false没有处理事件

                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 
            //省略部分代码


//dispatchTransformedTouchEvent方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

       
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                //自己处理事件
                handled = super.dispatchTouchEvent(event);
            } else {
                //调用子View的dispatchTouchEvent
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
}

具体流程可以看注释,这里做个小结.

  1. 当寻找到可以处理点击事件的子View时,那么就会将事件传递给子View.
  2. 如果没有找到子View或者子View没有处理事件,那么将由自身来处理.

View的处理过程

对应源码

public boolean dispatchTouchEvent(MotionEvent event) {
        //...省略部分代码

        boolean result = false;
       
        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)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {//onTouch返回false时
                result = true;
            }
        }

        return result;
    }



最后对上面流程做一个小结.

  1. 如果设置了OnTouchListener的OnTouch方法.
    • 返回true,那么onTouchEvent不会调用.
    • 返回false,则回调onTouchEvent方法.
  2. 在onTouchEvent方法中,当UP事件时,会回调OnClickListener的Onclcik方法.

由此可见优先级为: OnTouch > onTouchEvent > Onclcik。

由于本人技术有限,如有错误的地方,麻烦大家给我提出来,本人不胜感激,大家一起学习进步.

参考链接: