深入理解android事件分发

引言

在Android开发中,经常要重写View的onTouchEvent方法来接收屏幕对View的点击事件,那这个点击事件是从哪里来的呢?整个点击事件又是怎么分发的呢? 我们在下面的内容中为大家解开谜团。

我把要讲的内容分为三部分:
1. 分发事件的由来
2. ViewGroup中事件的分发
3. View中事件的处理

1.分发事件的由来

说到Android的输入设备,包括触摸屏、键盘、按键等,游戏手柄、鼠标。当有输入设备接入的时候Linux会在/dev/input下创建相应的设备节点。当用户操作输入设备的时候,Linux内核会把输入事件数据写到相应的设备节点中,Android系统的InputManagerService服务(简称IMS)就会监控这些设备节点,当有数据时便将这些数据读取并加工成输入事件,并交给WindowManagerSerivce服务(简称WMS)处理,WMS会把选择合适的应用窗口(Window)进行派发,应用的ViewRootImpl就会接收到输入事件,并把输入事件进行分发给下面的View。

本章我们只从ViewRootImpl接收到输入事件做讲解,不会涉及native层和其他服务,并把重点放对屏幕触摸事件的处理。

ViewRootImpl中哪里接收到分发事件呢?还记得上一节对ViewRootImpl的setView操作吧:

ViewRootImpl#setView

  public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {



                //...
              res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);


                if (mInputChannel != null) {
                    if (mInputQueueCallback != null) {
                        mInputQueue = new InputQueue();
                        mInputQueueCallback.onInputQueueCreated(mInputQueue);
                    }
                    mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                            Looper.myLooper());
                }
               //...

            }
        }
    }

在通过addToDisplay方法把Window和WMS建立联系后,会在下面创建一个WindowInputEventReceiver对象,WindowInputEventReceiver继承抽象类InputEventReceiver,该类正是提供底层输入事件的接收。

当输入事件到来后native层会调用java层InputEventReceiver类的dispatchInputEvent方法:

InputEventReceiver#dispatchInputEvent

// Called from native code.
    @SuppressWarnings("unused")
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        onInputEvent(event);
}

该方法调用子类的onInputEvent方法

WindowInputEventReceiver #onInputEvent

final class WindowInputEventReceiver extends InputEventReceiver {
        public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }
        @Override
        public void onInputEvent(InputEvent event) {
            enqueueInputEvent(event, this, 0, true);
        }
      //...
    }

onInputEvent方法中则直接,调用了ViewRootImpl的enqueueInputEvent方法。这样事件正式的交给了ViewRootImpl进行处理了。

ViewRootImpl#enqueueInputEvent

void enqueueInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags, boolean processImmediately) {
        adjustInputEventForCompatibility(event);

        //把输入事件InputEvent和rceiver封装成一个QueuedInputEvent
        QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
        // Always enqueue the input event in order, regardless of its time stamp.
        // We do this because the application or the IME may inject key events
        // in response to touch events and we want to ensure that the injected keys
        // are processed in the order they were received and we cannot trust that
        // the time stamp of injected events are monotonic.

        //以链表的方式存储QueuedInputEvent对象
        QueuedInputEvent last = mPendingInputEventTail;
        if (last == null) {
            mPendingInputEventHead = q;
            mPendingInputEventTail = q;
        } else {
            last.mNext = q;
            mPendingInputEventTail = q;
        }
        //记录个数
        mPendingInputEventCount += 1;
        Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                mPendingInputEventCount);
        if (processImmediately) {
            //立即处理
            doProcessInputEvents();
        } else {
           //通过Handler处理
            scheduleProcessInputEvents();
        }
    }

因为我们传入processImmediately为true,表示立即处理,所以我们最后执行doProcessInputEvents方法。scheduleProcessInputEvents则是安排ViewRootHandler去处理,通过Handler最终还是调用doProcessInputEvents方法。

ViewRootImpl#doProcessInputEvents

void doProcessInputEvents() {
        // Deliver all pending input events in the queue.
        while (mPendingInputEventHead != null) {
            //从链表头部依次取出
            QueuedInputEvent q = mPendingInputEventHead;
            mPendingInputEventHead = q.mNext;
            if (mPendingInputEventHead == null) {
                mPendingInputEventTail = null;
            }
            q.mNext = null;
            //总数减1
            mPendingInputEventCount -= 1;
            Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                    mPendingInputEventCount);
            long eventTime = q.mEvent.getEventTimeNano();
            long oldestEventTime = eventTime;
            if (q.mEvent instanceof MotionEvent) {
                MotionEvent me = (MotionEvent)q.mEvent;
                if (me.getHistorySize() > 0) {
                    oldestEventTime = me.getHistoricalEventTimeNano(0);
                }
            }
            mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
            deliverInputEvent(q);
        }
        // We are done processing all input events that we can process right now
        // so we can clear the pending flag immediately.
        if (mProcessInputEventsScheduled) {
            mProcessInputEventsScheduled = false;
            mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
        }
    }

在doProcessInputEvents方法中遍历循环整个QueuedInputEvent链表,调用deliverInputEvent处理每个输入事件。

ViewRootImpl#doProcessInputEvents

private void deliverInputEvent(QueuedInputEvent q) {
        Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
                q.mEvent.getSequenceNumber());
       //检测输入事件是否前后一致
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
        }
        InputStage stage;
       //因为我们flag为0,所以shouldSendToSynthesizer返回false
        if (q.shouldSendToSynthesizer()) {
            stage = mSyntheticInputStage;
        } else {
            stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
        }
        if (stage != null) {
            stage.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }

QueuedInputEvent 会根据shouldSkipIme的返回执行mFirstPostImeInputStage或 mFirstInputStage其中一个变量,这两个变量都是InputStage类型,在setView的时候进行创建,InputStage类设计就是责任链模式,通过deliver方法,一层层的传递,最后是传递到ViewPostImeInputStage类中来处理,处理方法是processPointerEvent方法:

ViewPostImeInputStage#processPointerEvent

private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;
            mAttachInfo.mUnbufferedDispatchRequested = false;
            boolean handled = mView.dispatchPointerEvent(event);
            if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
                mUnbufferedInputDispatch = true;
                if (mConsumeBatchedInputScheduled) {
                    scheduleConsumeBatchedInputImmediately();
                }
            }
            return handled ? FINISH_HANDLED : FORWARD;
        }

在该方法中mView变量,就是在setView方法中传进来的DecroView,我们调用DecroView的dispatchPointerEvent方法进行处理,而DecroView并没有重写dispatchPointerEvent方法,该方法由父类View来实现:

View#dispatchPointerEvent

public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
}

我们看到该方法是final修饰,所以不能重写该方法,只能继承。该方法判断是否是触摸事件,因为我们本章关注的是触摸方法,所以我们执行dispatchTouchEvent方法,而DecorView重写了该方法:

DecorView#dispatchTouchEvent

 @Override
 public boolean dispatchTouchEvent(MotionEvent ev) {
            final Callback cb = getCallback();
            return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)
                    : super.dispatchTouchEvent(ev);
}

因为DecorView是PhoneWindow的内部类,getCallback()获取的是Window的Callback接口,还记得我们的Callback接口是什么时候设置的吗?就在Activity的attach方法中,我们让Activity实现了Window的Callback接口,不知道大家还有没有记得。如果Callback有设置,那我们就调用Activity的dispatchTouchEvent方法,输入事件现在传递到了Activity中,我们看看Activity的dispatchTouchEvent方法到底做了什么

Activity#dispatchTouchEvent

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

该方法中会调用该Activity的Window去执行的superDispatchTouchEvent方法,如果superDispatchTouchEvent返回true,那就表示输入事件有被Activity的View成功处理。如果返回false,则表示没有任何的View处理该输入事件,那就会直接交给Activity的onTouchEvent进行处理,我们先看看onTouchEvent是如何处理没有被View处理的输入事件:

Activity#onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
}

在该方法中,会判断我们的点击事件是不是应该在超过Window边界的进行关闭,如果是则关闭该Activity,返回true,表示事件已经处理,否则返回false。

我们再看看superDispatchTouchEvent方法是如何把输入事件继续往下分发的:

PhoneWindow#superDispatchTouchEvent

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

PhoneWindow中调直接用了DecorView的superDispatchTouchEvent方法:

DecorView#superDispatchTouchEvent

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

DecorView方法中则是调用了父类的dispatchTouchEvent方法,因为DecorView继承FrameLayout,FrameLayout继承ViewGroup,而FrameLayout没有重写该方法,所以DecorView调用的是ViewGroup的dispatchTouchEvent方法。到此方法整个View的事件分发正式开始。

通过上一章对Activity的分析我们知道整个Activity的构成如下:
这里写图片描述
从DecorView开始往下的整个体系都是View的嵌套,从DecorView开始才真正的事件在View中分发。

对上面源码的分析中可以知道一个输入事件的分发,正如上图所示从最外层Activity一层层的传递进去。

我们用一张图来表示上面源码分析的过程
这里写图片描述

在进入View事件分发的开始前,给大家先介绍下一些概念

  1. View是一个自己实例化的类,抽象类ViewGroup继承了View,ViewGroup不可以自己实例化。ViewGroup可以有多个子View,而且子View的类型也可以是ViewGroup,View表示单一控件并不能存在子 View。

  2. 当手指按到屏幕上后从滑动到抬起,我们称这些输入事件为同一事件序列。如果手指再次按到屏幕上,则表示新的事件序列开始。

  3. 如果输入事件有分发到一个View中,那么一定会调用该View的dispatchTouchEvent方法。ViewGroup和View的dispatchTouchEvent方法处理输入事件的方式不同,ViewGroup可以通过onInterceptTouchEvent方法拦截往下分发的输入事件,因为View最底层的控件,所以没有拦截事件也就没有onInterceptTouchEvent方法。

2.ViewGroup中事件的分发

在上面最后DecorView调用了ViewGroup的dispatchTouchEvent方法,我们看看ViewGroup到底对输入事件做了哪些处理:

ViewGroup#dispatchTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

       //....

            boolean handled = false;
            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;
            }

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

            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                  //...
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

                     //...
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        //...
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)? children[childIndex] : preorderedList.get(childIndex);
                            //...
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            //...
                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                 //...
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                                 //...
                        }
                    }
                         //...
                }
            }
            // 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;
                        } 
                         //...
                    }
                       //...
                }
            }
                    //...

        return handled;
    }

上面的方法过长,我把它分为下面几个部分从上到下依次讲解:

Step1:

            boolean handled = false;
            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();
            }

变量handled表示输入事件是否已被处理(消耗)

输入事件为MotionEvent类型,我们可以通过getAction获取输入事件的类型,但是该方法只能获取单点触摸的类型,如果想要获取多点触摸的类型可以通过getActionMasked方法,当然获取的类型也包括单点触摸的类型。actionMasked变量获取的方式其实就是getActionMasked方法的实现。

如果输入事件类型为ACTION_DOWN,那么就表示新的事件序列开始了,在这里会做两步部初始化操作:

第一步:执行cancelAndClearTouchTargets方法,该方法会清空并取消所有的TouchTarget。如果ViewGroup的子View成功的处理了输入事件,那么就会把该子View封装成TouchTarget对象,由mFirstTouchTarget变量持有。TouchTarget类是一个单链表结构,mFirstTouchTarget是否为null,将影响ViewGroup的拦截策略。因此如果是ACTION_DOWN事件,那就表示新的事件序列开始,我们就要清空ViewGroup中有关于上次事件序列的记录,通过cancelAndClearTouchTargets方法,我们先对mFirstTouchTarget变量中保存的View,发送ACTION_CANCEL事件,然后再对mFirstTouchTarget所保存的所有TouchTarget进行回收,并置mFirstTouchTarget为null。

cancelAndClearTouchTargets方法:

     /**
     * Cancels and clears all touch targets.
     */
    private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }
            //对所保存的View分发ACTION_CANCEL事件
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            //清空所有touch targets
            clearTouchTargets();
            if (syntheticEvent) {
                event.recycle();
            }
        }
    }

cancelAndClearTouchTargets方法最重要的操作还是把mFirstTouchTarget置为null。

第二步:执行resetTouchState方法,允许ViewGroup拦截输入事件。因为默认情况下父View是可以拦截输入事件的分发的,但是如果子View不希望它的父View拦截输入事件的时候,可以调用父View的requestDisallowInterceptTouchEvent方法并传入true,使父View不会拦截输入事件:

ViewGroup#requestDisallowInterceptTouchEvent

 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

mGroupFlags 是一个int变量,它记录了当前ViewGroup的各种状态。在上面的方法我们可以看出,该方法不仅对当前ViewGroup进行设置,而且对所有父ViewGroup都会进行设置,保证所有父View保持同一状态。

而调用resetTouchState方法,会重置mGroupFlags,使ViewGroup可以拦截事件的分发,使requestDisallowInterceptTouchEvent设置的状态无效。因此如果是ACTION_DOWN事件,那么子View调用requestDisallowInterceptTouchEvent并不会影响对父View的处理

因此Step1所做的事情很简,输入事件的类型为ACTION_DOWN,那么mFirstTouchTarget置为null,并允许ViewGroup拦截输入事件。

Step2:

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

intercepted变量表示当前ViewGroup是否拦截输入事件,如果为true表示当前ViewGroup就会进行拦截,不让事件继续往下分发。

从上面的方法我们知道,只有ACTION_DOWN或mFirstTouchTarget != null时(mFirstTouchTarget的作用Setp1已经说过),才会去判断是否要拦截当前事件。如果这两个都不成立那么intercepted为true,当前ViewGroup就会拦截输入事件。

if语句中disallowIntercept变量的作用是判断当前ViewGroup是否允许拦截事件,我在Step1有说过如果子View调用requestDisallowInterceptTouchEvent方法会改变ViewGroup拦截策略,就会影响disallowIntercept的值,因此如果是ACTION_DOWN事件前面会重置该策略,所以disallowIntercept为false。

onInterceptTouchEvent方法用于判断是否拦截输入事件,该方法在ViewGroup默认返回false,表示不拦截输入事件。我们可以重写该方法。

根据Step1和Step2我们可以总结如下几点:

  1. 如果事件为ACTION_DOWN类型,当前ViewGroup的mFirstTouchTarget肯定null,并且允许当前ViewGroup进行拦截(disallowIntercept为false),因此一定会调用onInterceptTouchEvent方法,该方法只有在ViewGroup中才有定义,该方法默认为false。

  2. 如果事件为ACTION_DOWN类型,并且onInterceptTouchEvent返回true,那么当前的输入事件就被拦截了,无法向下传递事件,所以mFirstTouchTarget为null。如果同一事件序列进来的时候,因为为非ACTION_DOWN事件,mFirstTouchTarget也为null,所以onInterceptTouchEvent没有被调用,intercepted被设置为true,继续被ViewGroup拦截。因此如果ViewGroup如果拦截ACTION_DOWN事件,那么后续的事件序列,都会被该ViewGroup拦截并处理。同一事件序列只能被一个ViewGroup拦截并且消耗。在此情况下onInterceptTouchEvent只会在ACTION_DOWN的时候被调用,后面的事件序列就不会再调用该方法了。

  3. 如果事件为ACTION_DOWN,onInterceptTouchEvent返回false,表示事件不被当前ViewGroup拦截,就会分发给子View处理。如果子View有做处理那么mFirstTouchTarget就不为null,那么后续同一事件序列再次进来的时候,每个事件都会调用onInterceptTouchEvent方法(前提是子View允许父View可以拦截)。

Step3:

        final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                  //...
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

                     //...
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        //...
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)? children[childIndex] : preorderedList.get(childIndex);
                            //...
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            //...
                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                 //...
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                                 //...
                        }
                    }
                         //...
                }
            }

在该代码段中,如果事件类型不为ACTION_CANCEL而且intercepted为false,表示ViewGroup不拦截输入事件需要向下分发,就会遍历ViewGroup中每一个子View,并调用dispatchTransformedTouchEvent方法去执行子View的dispatchTouchEvent方法。如果子View成功的处理了输入事件dispatchTransformedTouchEvent会返回true,然后通过addTouchTarget方法,把成功处理事件的子View封装成TouchTarget,并赋值给mFirstTouchTarget变量,mFirstTouchTarget实际上就是指向成功处理输入事件的View,接着会跳出循环完成遍历。如果所有子View都不处理输入事件那mFirstTouchTarget会为null。

在上面的代码中判断子View是否有资格接收输入事件的有两个方法,canViewReceivePointerEvents方法判断子View是否可见、以及是否在播放动画。isTransformedTouchPointInView方法判断点击事件的坐标是否落在子View区域内。只有这两个方法同时为true子View才有资格接收输入事件。

注意在上面的代码中,要遍历子View前必须还有一个判断那就是事件类型也必须为ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE其中之一。这要做有个好处是,当同一事件序列再次进来的时候,我们就不必每次都去遍历子View,而是直接交给mFirstTouchTarget变量中的View元素处理,提高了执行的效率和速度。这也说明了同一个事件序列只能被一个View拦截和消耗(前提父View不拦截)

Step4:

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

        return handled;

在上面的代码段中,mFirstTouchTarget为null的可能有哪些?

  1. 输入事件是ACTION_DOWN,ViewGroup拦截了输入事件(intercepted为true)
  2. 输入事件是ACTION_DOWN,ViewGroup所有的子View都没有消耗该事件

这时候就会调用dispatchTransformedTouchEvent方法,去执行当前ViewGroup的点击事件。

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


         //...
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
          //...
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        return handled;
    }

因为我们传入的child为null,所以执行了父类的dispatchTouchEvent方法。我们知道ViewGroup继承View,所以实际上是去调用了View的dispatchTouchEvent方法。

如果mFirstTouchTarget不为null,该Step4代码段的执行分为几种情况:

1.该事件为ACTION_DOWN事件,mFirstTouchTarget的View已经消耗了该事件。因为如果是ACTION_DOWN事件我们才可以进入Step3的循环遍历中,而且在里面已经消耗了该事件,就不必再执行事件了,所以在上面的代码中 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget)成立。

2.如果if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget)不成立,这就说明了输入事件,是ACTION_DONW的后续
事件,如ACTION_MOVE或ACTION_UP。这时候如果该ViewGroup要拦截该事件,那么cancelChild为true,在dispatchTransformedTouchEvent方法中,就把ACTION_CANCEL事件类型传递给mFirstTouchTarget中所指向的View,并把mFirstTouchTarget置为null,后续同一事件序列进来的时候,就会由ViewGroup处理了。如果ViewGroup不拦截该事件,那么那么cancelChild为false,在dispatchTransformedTouchEvent方法中,就会交给mFirstTouchTarget中所指向的View处理。

这里我们可以总结出几点:

  1. 如果某个View开始处理并消耗ACTION_DOWN事件,那么接来的事件序列,都会直接交给该View处理。如果后面的同一事件序列,该View不消耗的话,它的父View并不会处理,最终会交给Activity进行处理。

  2. 某个View如果不处理ACTIO_DOWN事件,那么后面的同一事件序列就不会交给该View处理了(无法接受到后面的事件),而是直接交给他的父类去处理。

  3. 如果View处理了ACTION_DOWN事件,当后面同一序列的事件到来时,该View的父类拦截这个事件,那么该View就会收到ACTION_CANCEL类型的事件。那么接下来同一序列的事件,该View都不会接收,而是直接交给他的父类处理。

ViewGroup中事件的分发到此结束了,我转了几份来自EOE 的图,帮助大家理解

  • ACTION_DOWN都没被消费
    这里写图片描述

  • ACTION_DOWN被View消费了
    这里写图片描述

  • 后续ACTION_MOVE和ACTION_UP在不被拦截的情况下都会去找View
    这里写图片描述

  • 后续的被父View拦截了
    这里写图片描述

  • ACTION_DOWN一开始就被拦截
    这里写图片描述

3.View中事件的处理

一个View如果接收到了输入事件,那就执行其dispatchTouchEvent方法。一个ViewGroup如果拦截了输入事件那也会执行View的dispatchTouchEvent方法。接下来我们看看,View的dispatchTouchEvent方法处理哪些事:

View#dispatchTouchEvent

    public boolean dispatchTouchEvent(MotionEvent event) {
        //... 

        boolean result = false;
        final int actionMasked = event.getActionMasked();

          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;
            }
        }
        //...
        return result;
    }

ListenerInfo是View的静态内类,用于存储所有监听接口的对象,比如OnClickListener、OnTouchListener、OnLongClickListener等接口的对象,这些接口创建后都会存储在ListenerInfo 中。

在上面的代码中,如果当前View是Enable状态,而且有设置OnTouchListener接口,并且接口中的onTouch方法返回的是true,那么表示事件已被处理,onTouchEvent就不会被调用了。

这里我们可以得出OnTouchListener接口的优先级高于onTouchEvent方法,如果OnTouchListener的onTouch方法中成功处理了输入事件,那么onTouchEvent方法就不会被调用,但onTouch方法被调用的前提是该View处于enable。

我们看看View的onTouchEvent方法执行了哪些操作,因为onTouchEvent方法过长,我把它分为下面几个部分从上到下依次讲解:

Step1:

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

mViewFlags保存了该View的信息状态,比如是否可以点击、是否Enable、是否可以长按等,与ViewGroup的mGroupFlags变量类似。

如果该View处于disable的状态,但是还是会消耗事件,只要CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE其中一个可用即可。

在上面的代码我们还可以知道View设置有代理mTouchDelegate ,就会把输入事件交给代理mTouchDelegate 处理。

Step2:

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:
                    mHasPerformedLongPress = false;
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }
                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();
                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);
                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();
                            setPressed(false);
                        }
                    }
                    break;
            }
            return true;
        }
        return false;

在上面的代码段中我们可以看出只要View的CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE这三种状态只要其中一个可用,那么事件就会被消耗,否则事件不会被消耗。我们看看DOWN、MOVE、UP事件分别对View做了那些处理。

ACTION_DOWN:
在对ACTION_DOWN的事件中,主要做以下的两个事件:
1.调用setPressed方法,把当前View设置为pressed状态。
2.调用checkForLongClick方法。如果LONG_CLICKABLE可用的话,会先延迟一段时间(默认500毫秒)后,如果该View还是处于pressed状态,就会调用OnLongClickListener接口(如果有设置)的onLongClick方法。
ACTION_DOWN中还调用了isInScrollingContainer方法,该方法主要的作用是判断该View是否处于可滑动的父类中(像LinearLayout为false,ScrollView为true,默认为true),如果是就会延迟一段的时间(默认100毫秒)再执行以上的两个事件。这样做的目的是:This prevents the pressed state from appearing when the user is actually trying to scroll the content。如果父类是不可滑动的ViewGroup,我们不必延迟处理。

ACTION_MOVE:
ACTION_MOVE的操作很简单,首先通过pointInView判断输入事件坐标是不是坐落于该View区域内,如果不是则把该View状体变为非pressed,并移除OnLongClickListener接口的onLongClick方法的调用,如果没有超过延迟时间的话。

ACTION_UP:
对于ACTION_UP事件,首先会判断该View是否处于pressed状态,如果不是则退出,否则继续处理。如果长按事件还未发生,那么就会移除对长按事件的调用,并执行performClick方法,该方法会执行OnClickListener接口的onClick方法。如果长按事件已经触发,就不会调用OnClickListener接口的onClick方法。最后把View设置为非pressed状态。

这里要对View的dispatchTouchEvent总结几点:

  1. View的CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE这三个状态,只要其中一个状态可用,就会消耗输入事件,否则View无法消耗输入事件

  2. View的disable和enable状态并不会影响,输入事件的消耗。但如果是disable状态,OnClickListener、OnLongClickListener、OnTouchListener等监听事件都不会被调用,因为根本无法进入Step2。

  3. OnLongClickListener的触发只需要ACTION_DOWN事件,而OnClickListener的触发需要ACTION_DOWN事件和ACTION_UP事件,OnLongClickListener和OnClickListener不可能在同一事件序列中同时出现。

  4. OnTouchListener接口调用的优先级高于onTouchEvent方法,如果OnTouchListener的onTouch方法中成功处理了输入事件,那么onTouchEvent方法就不会被调用,但onTouch方法被调用的前提是该View处于enable状态。

  5. 对View(ViewGroup)进行setOnClickListener和setOnLongClickListener,都会让View(ViewGroup)置于CLICKABLE和 LONG_CLICKABLE状态,而setOnTouchListener并不会改变任何状态。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值