Android MotionEvent分发机制

大纲

一、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);
    }

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容