Android MotionEvent分发机制

作者: PuHJ | 来源:发表于2019-03-23 19:36 被阅读70次
    大纲

    一、View基础知识

    1)、初识View

    Android体系中,View是承载了界面视图,它代表了一个矩形区域里面的内容。界面具体可分为两种类型View和ViewGroup,顾名思义ViewGroup中可以存放多个View。View称之为控件,分为系统提供的默认的控件和自定义的控件。

    2)、View位置

    借用网上的View坐标图,说明View中关于位置的属性和方法。因为View存在着相对位置,绝对位置以及动画时布局位置和绘画位置不一致导致的位置属性。


    View坐标
    MotionEvent位置参数
    • getX() : 鼠标点击的位置,距离该View的左边距距离
    • getY() : 鼠标点击的位置,距离该View的上边距距离
    • getRawX() : 鼠标点击的位置,距离该屏幕的左边距距离
    • getRawY() : 鼠标点击的位置,距离该屏幕的上边距距离
    • translationX : Animation动画在X轴移动的距离
    • translationY : Animation动画在Y轴移动的距离
    View位置参数
    • getLeft() : 该控件相对于父控件左边距
    • getTop() : 该控件相对于父控件上边距
    • getRight() : 该控件相对于父控件右边距
    • getBottom() : 该控件相对于父控件下边距
    • getX() : 控件相对父布局的左边距 + translationX
    • getY() : 控件相对父布局的上边距 + translationY

    3)、MotionEvent事件类

    MotionEvent类是保存了鼠标、键盘、触摸的一些属性类。

    对于触摸事件,他会产生一些具体的事件如Down、Move、Up、Cancel事件。其中Down,n个Move,Up算作一组事件。

    对于MotionEvent中的一些参数和方法,上面已经介绍过了。

    4)、设计模式之组合设计模式

    View体系中,ViewGroup是一个View组,继承自View,并且可以包含多个View。与文件夹和文件的含义一致。

    组合模式UML

    常见的组合模式就是一方持有另一个类的对象,View的这种是个特殊,持有的和被持有的基类都是一致的。下面用File和Folder举例说明组合模式。

    File类,基类,也是子节点。

    定义了两个属性文件名和文件大小。和该文件的父文件对象parent,这个是为了保证增删改文件的时候,对相应的文件夹更新更新,一般情况父文件夹的大小会变化。其中invalidate()类似于View中的刷新功能。

    public class File {
        
        // 文件名
        protected String name;
        // 文件大小
        protected int size;
        // 父节点
        protected File parent;
        
        // 打印的前缀
        protected final static String defaultPrefix = "----";
    
        public File(String name) {
            this.name = name;
        }
        
        public File(String name, int size) {
            this.name = name;
            this.size = size;
        }
        
        // 刷新大小
        protected void invalidate() {
            
        }
    
        @Override
        public String toString() {
            return "File [name=" + name + ", size=" + size + "]";
        }
    
    }
    
    Folder类,File类的派生类,文件夹可以包含多个文件或者文件夹。

    其中注意的地方就是folder中重写invalidate()。先计算自己的大小后,再通知父节点刷新,从下往上刷新。对应的View刷新是从下往上再从上往下。

    public class Folder extends File {
        
        public final List<File> fileList = new ArrayList<>();
    
        public Folder(String name) {
            super(name);
        }
        
        protected void invalidate() {
            // 1、先算自己的
            this.size = 0;
            for (File f : fileList) {
                this.size += f.size;
            }
            // 2、通知父节点计算
            if (this.parent != null) {
                this.parent.invalidate();
            }
        }
        
        // 增加一个文件夹
        public void addFile(File file) {
            if (file == null)
                return;
            file.parent =this;
            fileList.add(file);
            // 刷新大小
            invalidate();
        }
        
        // 遍历文件夹
        public void printFolder() {
            System.out.println(this.toString());
            print(fileList,defaultPrefix);
        }
        
        
        
        // 打印方法 
        private void print(List<File> fileList,String prefix) {
            for (File f : fileList) {
                if (f instanceof Folder) {
                    Folder folder = ((Folder) f);
                    System.out.println();
                    System.out.println(prefix+"Folder [name=" + folder.name + ", size=" + folder.size + "]");
                    if (!folder.fileList.isEmpty()) {
                        print(folder.fileList,prefix+defaultPrefix);
                    }
                    
                } else {
                    System.out.println(prefix+f.toString());
                }
            }
        }
    
    
        @Override
        public String toString() {
            return "Folder [name=" + name + ", size=" + size + "]";
        }
    }
    
    测试类Test

    再root目录下增加相应的节点,并打印输出。

        public static void main(String[] args) {
            Folder root = new Folder("root");
            
            Folder texts = new Folder("texts");
            texts.addFile(new File("text1",10));
            texts.addFile(new File("text2",10));
            
            Folder images = new Folder("images");
            images.addFile(new File("image1",20));
            images.addFile(new File("image2",30));
            
            Folder vedios = new Folder("vedios");
            vedios.addFile(new File("vedio1",20));
            vedios.addFile(new File("vedio2",30));
            
            root.addFile(texts);
            root.addFile(vedios);
            vedios.addFile(images);
            
            root.printFolder();
        }
    
    结果:
    结果

    二、MotionEvent分发流程

    MotionEvent的分发流程:Activity --> PhoneWindow --> DecorView --->ViewGroup --->View
    1)、Activity

    Android中,Activity是MotionEvent事件分发的唯一来源,当有触摸事件发生是,Linux通过判断当前是在哪个Activity,最终调用该Activity中的dispatchTouchEvent(MotionEvent event)方法,Event是从Activity的dispatchTouchEvent开始分发的。

    首先看下Activity中的Event分发方法:
    Activity作为Event分发的起点,首先是给子View处理,如果有View能处理,View处理完成后到此结束,如果没有任何View消费该事件,最后由Activity自己尝试消费处理

        public boolean dispatchTouchEvent(MotionEvent ev) {
            // 1、MotionEvent.ACTION_DOWN事件会通知用户,可通过重写onUserInteraction()接收
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            // 如果Window消费了该Event事件,就返回true,该事件的分发到此结束
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            // 如果没有任何View消费该事件,最后又Activity自己尝试消费处理
            return onTouchEvent(ev);
        }
    
    2)、Window

    Window是个抽象类,它的唯一一个派生类就是PhoneWindow。一个Activity会有一个PhoneWindow载体,在Activity的attach方法中会初始化一个PhoneWindow,PhoneWindow中会有一个DecorView(见PhoneWindow的构造方法)。本质Window中没做任何处理,全权由DecorView处理。

        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
        }
    
    3)、DecorView

    DecorView实际上就是一个ViewGroup,从类的声明来看,他继承了WindowCallbacks,这个是Window一些事件响应的回调。

    /** @hide */
    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
    

    DecorView何许人也?和我们的布局有何关联?
    正常情况下,Activity加载的是一个类似于下面的XML。外面的LinearLayout就是所说的DecorView。里面的id为content的则是我们在Activity中设置的XML。这样DecorView就和我们Activity的View联系起来了。

    这样Event事件则传到了ViewGroup的dispatchTouchEvent中


    View布局XML

    三、View中MotionEvent分发

    《Android开发艺术探索》中有段事件分发的伪代码,很精辟。

    public boolean dispatchTouchEvent(MotionEvent event) {
         boolean consume = false;
         if (onInterceptTouchEvent(event) {
              consume = onTouchEvent(event);
         } else {
              consume = child.dispatchTouchEvent(event);
         }
         return consume;
    }
    

    这段伪代码就描述了,View和ViewGroup中事件分发的伪代码。

    consume代表了是否消费的标志;最开始调用 onInterceptTouchEvent方法判断该ViewGroup是否需要拦截该Event,View中没有onInterceptTouchEvent,所以默认返回为true。如果拦截后(false),该Event就不会往下执行了,会调用自己的onTouchEvent(event),直接返回该消费的结果。如果没有拦截,那么则代表自己不会处理,需要交给自己的子View处理。子View再按照此结构分发。

    1)、dispatchTouchEvent(MotionEvent event)方法

    dispatchTouchEvent(MotionEvent event)是分发的开始。官方解释是,该方法是将MotionEvent事件向下传递到目标视图,或者如果它是目标,则将此视图传递给目标视图。

    View中的该方法比较精简,直接贴代码。

    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;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                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;
        }
    
    • result 代表了消费情况的标志,最终会被当做结果返回回去。
    • Event消费之前和消费之后,可能会调用stopNestedScroll();停止View滑动。
    核心逻辑
                ListenerInfo li = mListenerInfo;
    
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
    

    ListenerInfo类,保存View的一些接口对象,如View中的OnClickListener、OnLongClickListener等等

    如果 li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event),mOnTouchListener是View.setOnTouchListener()设置的触摸监听器,它的执行方法是onTouch,如果存在该接口对象以及该方法返回为true。还有个是ENABLED标志,可通过setEnable来设置标志位,但一般情况如果设置了点击和长点击的监听就会设置Enable为true,对于Button等View本身就是为true。

    如果onTouch返回的为true,代表这个event提前消费了,就不会走onTouchEvent方法了,否则就会走到onTouchEvent方法,并onTouchEvent的结果进行返回。

    也就是说提前设置mOnTouchListener监听,并在onTouch中返回true,就直接消费了。这样的话就不会走到onTouchEvent方法中。

    2)、onTouchEvent(MotionEvent event)

    如果dispatchTouchEvent方法中正常走到了onTouchEvent方法,代表该View可以在onTouchEvent中进行消费逻辑的判断了。

    下面给出了删减版的源码:

    public boolean onTouchEvent(MotionEvent event) {
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            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);
            }
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return 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)) {
                                        performClick();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
    
                            removeTapCallback();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                       XXXXXXXX.........
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        XXXXXXXX.........
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                       XXXXXXXX.........
                        break;
                }
    
                return true;
            }
    
            return false;
        }
    
    • 第一步,如果该View是Disable的,即(viewFlags & ENABLED_MASK) == DISABLED,这样也会代表该View消费了该事件。就会直接返回结果,不会再走之后的流程了。
    • 第二步,分别判断每个Event,进行不同的处理。Down、Move和 Cancel没有多少说的,重点看下Up事件。这里面最关键的几行就在:
    if (mPerformClick == null) {
         mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) {
        performClick();
     }
    
       // performClick方法
       public boolean performClick() {
            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);
            return result;
        }
    
        // 点击处理类
        private final class PerformClick implements Runnable {
            @Override
            public void run() {
                performClick();
            }
        }
    

    通过post()向主线程插入一个Message。相当于切换到主线程后,再执行performClick()方法。这个方法的意思就是判断有没有设置点击事件的监听,如果有通过li.mOnClickListener.onClick(this);通知给使用者。即在Up的事件中,执行了点击事件。

    四、ViewGroup中MotionEvent分发

    1)、ViewGroup

    public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
            }
    
            // If the event targets the accessibility focused view and this is it, start
            // normal event dispatch. Maybe a descendant is what will handle the click.
            if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
                ev.setTargetAccessibilityFocus(false);
            }
    
            boolean handled = false;
            if (onFilterTouchEventForSecurity(ev)) {
                final int action = ev.getAction();
                final int actionMasked = action & MotionEvent.ACTION_MASK;
    
                // Handle an initial 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();
                }
    
                // Check for interception.
                final boolean intercepted;
                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) {
    
                    // If the event is targeting accessiiblity focus we give it to the
                    // view that has accessibility focus and if it does not handle it
                    // we clear the flag and dispatch the event to all children as usual.
                    // We are looking up the accessibility focused host to avoid keeping
                    // state since these events are very rare.
                    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;
    
                        // Clean up earlier touch targets for this pointer id in case they
                        // have become out of sync.
                        removePointersFromTouchTargets(idBitsToAssign);
    
                        final int childrenCount = mChildrenCount;
                        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.
                            final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                            final boolean customOrder = preorderedList == null
                                    && isChildrenDrawingOrderEnabled();
                            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 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;
                                }
    
                                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);
                            }
                            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;
        }
    

    接下来再分析下,ViewGroup中的事件分发。其中handled是代表当前View及其子View的事件消费情况。

    第一步:清除标志位

    如果Down事件,代表一系列的事件的开始,先重置标志位。其中cancelAndClearTouchTargets(ev);主要将mFirstTouchTarget这个单链表。mFirstTouchTarget代表具体处理Event的系列View。resetTouchState();是重置一些状态标示。

    if (actionMasked == MotionEvent.ACTION_DOWN) {
          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;
                }
    

    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null),ACTION_DOWN事件或者mFirstTouchTarget 不为null会到第一个逻辑里面。否则intercepted = true;就拦截,让自己的

    ACTION_DOWN事件好理解,那么mFirstTouchTarget什么时候不为null了?当ViewGroup没有拦截View,给子View消费,而且子View也消费了Down事件。反之mFirstTouchTarget == null的情况则是该ViewGroup拦截了事件或者子View并没有消费Down事件。

    disallowIntercept是允许拦截的标志。View中调用getParent().requestDisallowInterceptTouchEvent(true),可以控制FLAG_DISALLOW_INTERCEPT标志位,从而达到让父View拦截的作用。通过调用requestDisallowInterceptTouchEvent方法,可以解决滑动冲突。

    如果disallowIntercept == false,那么intercepted = onInterceptTouchEvent(ev);否则intercepted = false

    3)、寻找可以处理的子View

    if(!canceled && !intercepted),也就是该事件没有取消以及没有拦截的情况下。会去通过坐标寻找那些View能够处理该MotionEvent,并将该View放在mFirstTouchTarget链中。

    for (int i = childrenCount - 1; i >= 0; i--)遍历的方式中,可以看到这些遍历是从最后添加的先遍历。因为后加的子View是显示在界面的最上面。

    遍历的缩减缩减代码如下:
    通过getTouchTarget判断该子View有没有添加到mFirstTouchTarget链中,如果添加了就break,这是避免多点出触碰的问题。接下来通过dispatchTransformedTouchEvent判断子View会不会消费掉Down事件,如果能消费就添加到mFirstTouchTarget链中,不能消费就跳过。

      newTouchTarget = getTouchTarget(child);
      if (newTouchTarget != null) {
          break;
      }
    
      if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
           newTouchTarget = addTouchTarget(child, idBitsToAssign);
           break;
     }
    

    4)、Event事件消费

    在遍历了子View之后,找到了可以消费的View,就让其做之后的消费处理。

    下面为消费的精简逻辑:

                if (mFirstTouchTarget == null) {
                    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;
                            }
                 
                        predecessor = target;
                        target = next;
                    }
                }
    

    首先分两种情况:mFirstTouchTarget == null 和 mFirstTouchTarget != null

    先看第一种mFirstTouchTarget == null,这种比较简单,为null代表没有子View能消费的,直接调用给该View的父View处理dispatchTouchEvent方法即可。

    第二种,针对Down事件:如果alreadyDispatchedToNewTouchTarget && target == newTouchTarget,则代表之前寻找的时候已经子View已经处理了该Down事件,就不需要重新处理了;第二种:Mowe和Up事件,通过调用dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)让其子View处理事件,并直接将结果返回。

    最后整个系列的事件结束了,执行了Up事件。或者该事件被取消了,就会重新的重置状态resetTouchState();

    ViewGroup#dispatchTransformedTouchEvent逻辑

    会分别对Cancel事件、多点触摸事件、正常MotionEvent事件分发处理。
    删减代码:

    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 {
                    handled = child.dispatchTouchEvent(event);
                }
                event.setAction(oldAction);
                return handled;
            }
    
            // Calculate the number of pointers to deliver.
            final int oldPointerIdBits = event.getPointerIdBits();
            final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
            if (newPointerIdBits == 0) {
                return false;
            }
          
            // 手指多点触发处理
            final MotionEvent transformedEvent;
            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);
            }
    
            // 正常的事件分发
            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;
        }
    

    无论是Cancel事件、多点触摸事件还是正常事件。他都是通过判断child此View是否为空判断的,如果child为空,则让父View去处理dispatchTouchEvent。反之则让该child处理dispatchTouchEvent。

    这样就可以在子View没处理的情况下,让其父View处理,达成了一个闭环。

    MotionEvent之cancel事件

    • 手指移出该View的范围
      前面分析了正常的事件分发,但是MotionEvent还有一个特殊的事件cancel。cancel代表者该一系列事件的取消。

    当手指点击了View,该View处理了Down事件(返回为true),手指继续滑动,直到滑动脱离了该View的区域,这个时候该View就会收到cancel。

    • 事件拦截
      首先子View处理了Down事件(返回为true),Move时候父View开始没拦截让子View处理,但中途时候拦截了该Move事件,这时候就会让子View收到该Cancel取消事件。

    五、滑动冲突解决方法

    1)、外部拦截法

    外部拦截法:即拦截逻辑放在父View,由父View决定要不要拦截,哪些事件会拦截。

    外部拦截法需要通过重写父View的onInterceptTouchEvent实现,伪代码如下(Android开发艺术中):

        private int mLastX = 0;
        private int mLastY = 0;
        
        @Override
        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:
                    int deltaX = x - mLastX;
                    int deltaY = y - mLastY;
                    if (Math.abs(deltaX) < Math.abs(deltaY))
                        intercepted = true;
                    else
                        intercepted= false;
                    break;
                case MotionEvent.ACTION_UP:
                    intercepted = false;
                    break;
                default:
                    break;
            }
            Log.e(TAG, "onInterceptTouchEvent: intercepted = "+intercepted );
    
            mLastY = y;
            mLastX = x;
            return intercepted;
        }
    

    上文大致意思是:通过两个变量记录上一次的手指的位置(X,Y)坐标,Down的时候记录最开始的位置,对于Down事件不能够拦截,必须为false,否则接下来的事件都会被拦截了。手指Move的时候,通过本次滑动位置,x轴和y轴移动位置的偏移量大小决定,由业务决定拦截或者不拦截。Up事件,也比较特殊,返回的是否拦截,会导致最终会不会让子View执行onTouch或者onClick回调方法。如果父View之前没有拦截,Up时候也没有拦截就会处理该回调方法,否则一概不会触发。

    2)、内部拦截法

    内部拦截法:内部拦截法,父View不需要处理是不是要拦截,一概由子View进行判断。当时分析的时候, ViewGroup#dispatchTouchEvent()#中final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;会判断会不会走ViewGroup中的onInterceptTouchEvent()方法。该标志位可以通过view.getParent().requestDisallowInterceptTouchEvent(boolean)来赋值。注意此方法不能只Down事件的时候调用,因为父View会重新的重置标志,导致标志为无效。

    伪代码:

     private int mLastX = 0;
        private int mLastY = 0;
        
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    int deltaX = x - mLastX;
                    int deltaY = y - mLastY;
                    if (Math.abs(deltaX) > Math.abs(deltaY)){
                        Log.e(TAG, "dispatchTouchEvent: " );
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    break;
                default:
                    break;
            }
    
            mLastY = y;
            mLastX = x;
            return super.dispatchTouchEvent(event);
        }
    

    此代码同外部拦截法原理一致。

    相关文章

      网友评论

        本文标题:Android MotionEvent分发机制

        本文链接:https://www.haomeiwen.com/subject/hskrvqtx.html