引言
在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事件分发的开始前,给大家先介绍下一些概念
View是一个自己实例化的类,抽象类ViewGroup继承了View,ViewGroup不可以自己实例化。ViewGroup可以有多个子View,而且子View的类型也可以是ViewGroup,View表示单一控件并不能存在子 View。
当手指按到屏幕上后从滑动到抬起,我们称这些输入事件为同一事件序列。如果手指再次按到屏幕上,则表示新的事件序列开始。
如果输入事件有分发到一个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我们可以总结如下几点:
如果事件为ACTION_DOWN类型,当前ViewGroup的mFirstTouchTarget肯定null,并且允许当前ViewGroup进行拦截(disallowIntercept为false),因此一定会调用onInterceptTouchEvent方法,该方法只有在ViewGroup中才有定义,该方法默认为false。
如果事件为ACTION_DOWN类型,并且onInterceptTouchEvent返回true,那么当前的输入事件就被拦截了,无法向下传递事件,所以mFirstTouchTarget为null。如果同一事件序列进来的时候,因为为非ACTION_DOWN事件,mFirstTouchTarget也为null,所以onInterceptTouchEvent没有被调用,intercepted被设置为true,继续被ViewGroup拦截。因此如果ViewGroup如果拦截ACTION_DOWN事件,那么后续的事件序列,都会被该ViewGroup拦截并处理。同一事件序列只能被一个ViewGroup拦截并且消耗。在此情况下onInterceptTouchEvent只会在ACTION_DOWN的时候被调用,后面的事件序列就不会再调用该方法了。
如果事件为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的可能有哪些?
- 输入事件是ACTION_DOWN,ViewGroup拦截了输入事件(intercepted为true)
- 输入事件是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处理。
这里我们可以总结出几点:
如果某个View开始处理并消耗ACTION_DOWN事件,那么接来的事件序列,都会直接交给该View处理。如果后面的同一事件序列,该View不消耗的话,它的父View并不会处理,最终会交给Activity进行处理。
某个View如果不处理ACTIO_DOWN事件,那么后面的同一事件序列就不会交给该View处理了(无法接受到后面的事件),而是直接交给他的父类去处理。
如果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总结几点:
View的CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE这三个状态,只要其中一个状态可用,就会消耗输入事件,否则View无法消耗输入事件。
View的disable和enable状态并不会影响,输入事件的消耗。但如果是disable状态,OnClickListener、OnLongClickListener、OnTouchListener等监听事件都不会被调用,因为根本无法进入Step2。
OnLongClickListener的触发只需要ACTION_DOWN事件,而OnClickListener的触发需要ACTION_DOWN事件和ACTION_UP事件,OnLongClickListener和OnClickListener不可能在同一事件序列中同时出现。
OnTouchListener接口调用的优先级高于onTouchEvent方法,如果OnTouchListener的onTouch方法中成功处理了输入事件,那么onTouchEvent方法就不会被调用,但onTouch方法被调用的前提是该View处于enable状态。
对View(ViewGroup)进行setOnClickListener和setOnLongClickListener,都会让View(ViewGroup)置于CLICKABLE和 LONG_CLICKABLE状态,而setOnTouchListener并不会改变任何状态。