这一个知识点也是写烂了的,可是作为 Android 开发者又不得不学习这部分,学习了呢,总觉得要写点东西出来才觉得有感觉,得,就有这一篇文章了。
API 27
流程介绍
在单点触摸中,我们对屏幕的点击,滑动,抬起等一系的动作都是由一个一个MotionEvent对象组成的触摸事件。MotionEvent 是对一个对一个事件的封装,里面包括动作、坐标等等信息,根据不同动作,主要有以下三种事件类型:
- ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
- ACTION_MOVE:手指在屏幕上移动时候产生该事件
- ACTION_UP:手指从屏幕上松开的瞬间产生该事件
要要注意触摸事件不是独立的,而是成组的,每一组事件都是由按下事件开始的,由抬起事件或者取消事件结束。我们把由 ACTION_DOWN 开始(按下),ACTION_UP (抬起)或者 ACTION_CANCEL(取消) 结束的一组事件称为事件序列或者说事件流。取消事件是一种特殊的事件,它对应的是事件序列非人为的提前结束。
举个例子:
点击事件:ACTION_DOWN -> ACTION_UP
滑动事件:ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
Android 每产生一个 TouchEvent 事件,他会先问最表面是否消费,如果不消费就交给他的ViewGroup,一层一层向上传递,最终被消费掉(消费就是以为着事件被处理了,代码体现为返回值,true为消费,false为不消费,消费后不再传递)。TouchEvent 不断产生,事件就会不断分发,处理,实现对事件对应的操作进行判断和反馈处理。
还是举个栗子:
一个button被点击一下,就会产生两个 TouchEvent 事件,当第一个 TouchEvent 产生,button 发现自己被按下,背景风格变成按下状态,如水波纹、颜色变深等。当第二个Up 的 TouchEvent 产生、分发的时候,button判别自己被点击,背景风格恢复默认状态,并且如果设置了ClickListener
的话,调用 OnClick
方法。
那么如果你的ViewGroup里面不止一个View呢(不是废话吗),不止一个ViewGroup呢?那是不是我就要制定一个机制来决定谁来处理这个事件啊?安排
当事件刚触摸到屏幕的时候,即 ACTION_DOWN 这个 MotionEvent 产生的时候,如果ViewGroup中的View消费(返回true),就将这个View记录下来。后续这一个事件流都直接交给它处理。
事件分发机制-简图.png其实只有 ACTION_DOWN 事件需要返回 true,其后的像 UP啊,Move啊,他们的返回值并没有什么影响,但是还是推荐都写成true,降低维护成本。
当情况复杂,比如说你现在操作的是列表,点一下会触发点击事件,滑一下就会滑动,那么这样的隔着一个View如何实现的呢?这就是依靠着的就是事件拦截机制。
我们将这个过程细分,当你触摸的时候(DOWN事件),这个事件其实是先传到Activity、再传到ViewGroup、最终再传到 View,先问问ViewGroup你拦不拦截啊?一层一层的向下问,如果拦截呢,就直接交给他,如果不拦截呢?就直接往下传,直到传到底层的View,底层的View没有拦截方法,直接问他消不消费,不消费,向上分发,问他的ViewGroup是否分发,如果消费就直接交给它消费掉。这样的话,就可以把消费的权力先交给子View,在合适的时候父View可以马上接管过来。
那么滑动的过程呢?就是在DOWN事件发生的时候,先交给子View消费,当出现MOVE事件的时候,列表发现这个是滑动,需要自己处理,就拦截并且消费掉。但是这时候View还等着后续的事件流,就比如说背景风格还是按下状态,那么父View就会发给它一个cancel事件,让他恢复状态,并且后续事件交给拦截的父View来处理。
事件分发拦截机制-详细图解
始于 Activity
点击事件产生最先传递到当前的Activity,由Acivity的dispatchTouchEvent方法来对事件进行分发。
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
代码很简单,我们来一行一行进行解析。最开始,就是就是判断当前这个事件是否是按下这个事件( MotionEvent.ACTION_DOWN
),如果是,就执行一个空方法( onUserInteraction()
等待程序猿大爷重写)
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activity's {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
这里多说几句,这个空方法是在哪些时候会调用呢?毕竟我们也是要重写的嘛,那就必须知道其执行的时期:activity在分发各种事件的时候会调用该方法,旨在提供帮助Activity智能地管理状态栏通知。当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器,就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,自己消失等。
我们接着往下看getWindow().superDispatchTouchEvent(ev)
:
public Window getWindow() {
return mWindow;
}
直接返回当前界面的 mWindow,mWindow 是什么啊,是 Window ,Window 我们都知道,是一个 抽象类,它的唯一实现类就是 PhoneWindow,那我们来点一下 superDispatchTouchEvent(MotionEvent)
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
Window 的抽象方法啊,那我们在 PhoneWindow找一找
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
哇,实现要不要就这么简单,直接由Window 直接传递给了 mDecor,mDecor是什么啊?是 DecorView。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker,
WindowCallbacks {
}
DecorView就是Window的顶级View,是一个ViewGroup,我们通过setContentView设置的View是它的子View(Activity的setContentView,最终是调用PhoneWindow的setContentView).
这里放一张 Activity->视图 的图片
Activity 结构
是不是简单几步就实现了由Activity到ViewGroup的传递,这个中间传递者呢,就是Window。
上面传递到了 DecorView,他直接调用了 ViewGroup 的dispatchTouchEvent()
进行分发了。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
在陷入复杂的分发逻辑之前,我们先看 Acivity#dispatchTouchEvent留下的一个尾巴 -- 最后这个return onTouchEvent(ev);
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) { // 当超出边界要关闭Window,且超出边界,且顶层的 DecorView 不为空
finish();
return true;
}
return false; // 默认情况
}
Activity#onTouchEvent 是我们经常重写的方法,执行了 onTouchEvent
表示 getWindow().superDispatchTouchEvent(ev)
返回的是 false,我们都知道在事件分发体系中,true 表示消费了这个事件(处理了这个事件),那么onTouchEvent 被调用表示这个事件没有任何View消费,只能交给 Activity 处理,如何处理?就是调用 onTouchEvent 这个方法。
来看一下Window#shouldCloseOnTouch
/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
这里判断mCloseOnTouchOutside标记及是否为ACTION_DOWN事件,同时判断event的x、y坐标是不是超出Bounds,然后检查FrameLayout的content的id的DecorView是否为空,进行简单判断,由此决定是否销毁这个 Activity。
到这里 Activity 这一层就分析完了。我们在这里理一下:
- 先判断是否是按下事件,是则 调用
onUserInteraction();
空方法 - 在 if 括号中分发,首先是交给Activity上的 Window,Window交给顶级视图 DecorView,DecorView 调用父类 ViewGroup#dispatchTouchEvent 进行分发。
- 如果在分发结束后,没人消费这个事件,就调用Activity#onTouchEvent 进行处理,处理得很简单,就是判断是否需要超出边界就销毁当前的Activity,需要且超出边界就finish 并且返回true,默认为false。
ViewGroup
书接上文,当我们将事件交给 ViewGroup#dispatchTouchEvent ,那他怎么处理的呢?
真的可以说是超级长了,墙裂推荐使用编辑器看。还有就是看注释。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 检查合法性代码省略
boolean handled = false; // 是否消费
if (onFilterTouchEventForSecurity(ev)) { // 以安全策略判断是否可以分发,true->可以分发
final int action = ev.getAction(); // 事件动作 不同的位存储有不同的信息
final int actionMasked = action & MotionEvent.ACTION_MASK; // 事件类型
// 注释1
// 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); // 将当前事件分发下去,并且将整个TouchTarget链表回收
resetTouchState(); // 重置Touch状态标识
}
// Check for interception. 标记ViewGroup是否拦截Touch事件的传递
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // 当事件是按下或者已经找到能够接收touch事件的目标组件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 是否禁止拦截 注释2
if (!disallowIntercept) { // 如果自己可以拦截,默认可以
intercepted = onInterceptTouchEvent(ev); // 注释3 默认不拦截,用于重写
ev.setAction(action); // restore action in case it was changed
} else { // 不可以拦截,直接将intercepted 设置为false
intercepted = false;
}
} else { // 注意,重点,当不是事件序列开始,而且还没有设置分发的子View,那么只有一种可能,就是在这之前就被我自己拦截过了,后续序列我默认拦截消费
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
// 不是事件流开始的 ACTION_DOWN,也没有事件流的消费组件,那么直接拦截。
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. 检查 cancel 事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// 开始事件分发
// Update list of touch targets for pointer down, if needed.
// 是否把事件分发给多个子View,设置: ViewGroup#setMotionEventSplittingEnabled
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null; // 用于存储已经是事件流承受者的TargetView(在mFirstTouchTarget 这个事件流消费者链表中)
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;
// 处理ACTION_DOWN事件
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
// 当前 MotionEvent 的动作标识
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; // 子View数量
if (newTouchTarget == null && childrenCount != 0) { // 有子View可分发
final float x = ev.getX(actionIndex); // 得到点击的X坐标
final float y = ev.getY(actionIndex); // 得到y坐标
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList(); // 子View的集合 注释4(顺序问题)
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren; // 也是所有子View
for (int i = childrenCount - 1; i >= 0; i--) { // 倒序访问
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder); // 得到下标,正常情况下就是 i
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex); // 取出 i 对用的View
// 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) // 注意,这就是主要的筛选条件:1. 能不能接收事件(不可见或者在动画)
|| !isTransformedTouchPointInView(x, y, child, null)) { // 2. 是不是在他的范围内
ev.setTargetAccessibilityFocus(false);
continue;
}
// 注释5 如果在 mFirstTouchTarget中,就返回当前这个封装了child 的 TouchTarget,没有就返回null(注意,这时候这个View已经是在)
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) { // 在mFirstTouchTarget 这个事件流消费者链表中,找到事件流的消费者,跳出循环
// Child is already receiving touch within its bounds.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break; // 像UP、MOVE等事件就是从这里跳出循环的
}
resetCancelNextUpFlag(child); // 重置flag:cancel next up
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 注释6 重中之重 就是这里分发,看子View是否消费
// Child wants to receive touch within its bounds. 如果消费了
mLastTouchDownTime = ev.getDownTime(); // 更新按下事件
if (preorderedList != null) {
// childIndex points into presorted list, find original index
// 找到在ViewGroup 中存储的child,最原始的下标
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j; // 找到ViewGroup 中的数组的原始下标,保存在ViewGroup的成员变量中
break;
}
}
} else { // 临时的排过序的数组为null
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX(); // 被消费的事件流的DOWN事件的触摸点X(起点x坐标)
mLastTouchDownY = ev.getY(); // 起点y坐标
newTouchTarget = addTouchTarget(child, idBitsToAssign); // 将消费事件流的子View的父View(当前ViewGroup)记录在消费的链表头 插入操作可见注释7
alreadyDispatchedToNewTouchTarget = true; // 表示已经成功分发给自己的子View
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
} // for循环结束
if (preorderedList != null) preorderedList.clear();
} // 处理是 if (newTouchTarget == null && childrenCount != 0),意味着子View不为0并且没有记录的情况下的处理
// dispatchTransformedTouchEvent方法返回false,意味着子View也不消费
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.没有child接收事件
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
} // DOWN 事件的处理结束
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) { // 子View不消费
// 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) { // 这两个值是在第一次dispatchTransformedTouchEvent的时候返回true赋值的,意味着事件被子View消费
handled = true; // 如果被消费了
} else {
// 不分发给子View,意味着被拦截或者子View与父ViewGroup临时视图分离(mPrivateFlags设置了PFLAG_CANCEL_NEXT_UP_EVENT),就向记录在的
// 是否分发给子View
final boolean cancelChild =
resetCancelNextUpFlag(target.child)
|| intercepted; // 当前ViewGroup是否拦截
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) { // 如果不分发分发子View,调用dispatchTransformedTouchEvent发送cancel事件,已经分发过了就排除新的触摸目标
handled = true; // 是否自己或者子View消费
}
if (cancelChild) { // 事件不分发给子View,有可能是被拦截了
if (predecessor == null) { // 具体链表操作看 注释8
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;
}
注释1:
这里呢,就是当一个 ACTION_DOWN 事件来了以后,需要清除一些以前事件序列的标记,开始下一个事件序列。在 cancelAndClearTouchTargets(ev) 方法中有一个非常重要的操作就是将mFirstTouchTarget设置为了null,在resetTouchState()方法中重置Touch状态标识。
mFirstTouchTarget 是 TouchTarget,ViewGroup 的成员变量,记录要消费整个事件流的View,一个触摸事件可能有多个View可以接收到,该参数把他们连接成链状。
注释2
这里介绍一下几个基础知识,让大家知道为什么有这个事件拦截。
当我们按下的时候,即 ACTION_DOWN 发生的时候,标志着整个事件流的开始,这时候我们会去找整个事件流的处理者,对应的就是整个事件分发流程,一旦找到这个事件流的处理者(消费了这个事件的ACTION_DOWN),那么后续的整个事件流都会直接发送给这个处理者进行消费掉。
就比如说屏幕上有一个button,我滑动一下按钮,则从 ACTION_DOWN 的时候找到消费这个事件的组件了,然后button表现出按下状态。而后续整个 ACTION_MOVE 事件和 ACTION_UP 事件都直接发送给这个button处理。当下一个事件流来到又重复上述过程。
当情况变复杂的时候,比如说是列表,首先一来就是一个 ACTION_DOWN 事件,可是我也不知道他是点击还是按下啊,所以只能分发下去,交给了item消费了,可是我发现他是滑动事件,那么我就要从子View 中把消费事件的权利抢过来,就是拦截了。而item呢?还是一个按下状态,就发送一个 ACTION_CANCEL 事件给他让他恢复状态。这里呢,意思就是说,当一个事件流我交给子View消费过后,后续不再分发给我,但是在整个事件流处理过程中,我可以随时拦截,交给我来处理。
而假如我是子View,我又不希望我的ViewGroup拦截怎么办呢?当然有办法: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);
}
}
很简单,设置 ViewGroup的标志位,并递归告诉父ViewGroup不要拦截。
注释3
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
在当前ViewGroup可以拦截的情况下,看自己拦不拦截呢?不拦截,鼠标那个事件就不考虑了,看到没有,默认返回false,不拦截。当然这个方法主要也是用于我们重写。
注释4
preorderedList中的顺序:按照addView或者XML布局文件中的顺序来的,后addView添加的子View,会添加在列表的后面,会因为Android的UI后刷新机制显示在上层;
在事件分发的时候倒序遍历分发,那么最上层的View就可以最先接收到这个事件流,并决定是否消费这个事件流。
注释5
/**
* Gets the touch target for specified child view.
* Returns null if not found.
*/
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
从这里我们可以很清楚的明白,首先存储消费事件的目标组件的数据结构是链表,其次 mFirstTouchTarget 就是头节点。而 getTouchTarget 就是遍历整个链表,如果有就返回这个TouchTarget,没有就返回null,最后返回的值存储在 newTouchTarget 中。
这里我们介绍一下 TouchTarget ,TouchTarget 作为 ViewGroup 的内部类,原理很像Message的原理。Android 的消息机制 介绍传送门
/* Describes a touched view and the ids of the pointers that it has captured.
*
* This code assumes that pointer ids are always in the range 0..31 such that
* it can use a bitfield to track which pointer ids are present.
* As it happens, the lower layers of the input dispatch pipeline also use the
* same trick so the assumption should be safe here...
*/
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32; // 回收池最大容量
private static final Object sRecycleLock = new Object[0]; // 回收时候同步控制需要持有的对象锁
private static TouchTarget sRecycleBin; // 回收池的头节点,注意是 static
private static int sRecycledCount; // 当前回收池的数量
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
public View child; //存储的数据:View。整个事件流的消费者
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
public TouchTarget next; //下一个节点
private TouchTarget() { // 不能在外部new出来
}
// 将传入的数据封装成一个TouchTarget链表的结点
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) { // 需要传入封装的对象吖
throw new IllegalArgumentException("child must be non-null");
}
final TouchTarget target; // 最后构建出来存储的链表节点
synchronized (sRecycleLock) { // 拿到同步锁
if (sRecycleBin == null) {
target = new TouchTarget(); // 回收池为空,直接内部new出来
} else {
target = sRecycleBin; // 将头节点作为目标节点
sRecycleBin = target.next; // 将头节点下移一个
sRecycledCount--; // 回收池数量减一
target.next = null; // 将取出的节点与链表的联系断掉
}
}
target.child = child; // 装进节点
target.pointerIdBits = pointerIdBits;
return target;
}
// 提供回收当前节点的方法
public void recycle() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}
synchronized (sRecycleLock) { // 拿到同步锁
if (sRecycledCount < MAX_RECYCLED) { // 没有超过回收池容量
next = sRecycleBin; // 当前回收节点指向回收池链表的头结点
sRecycleBin = this; // 回收池头结点指向自己,相当于上移
sRecycledCount += 1; // 数量加1
} else {
next = null; // 置空,help Gc
}
child = null; // 抹除记录的数据
}
}
}
既然最后是一条以为头结点的链表,那么他到底存的是哪些View呢?上一张图:
mFirstTouchTarget 链表当我们按下 button2 的时候,会一层一层的传下去,最下层的消费了,然后返回上层接着执行代码(方法调用的时候是当前方法就被压入栈中,调用方法执行结束再弹出执行),上层会在 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
的时候得到true,将刚刚消费的子View(ViewGroup/View)记录进链表。
注释6
下面就是在第一次什么都没有的时候进行分发,注意哦,这里还在循环里面,就意味着这次循环没找到记录,并且触摸点在这个ViewGroup范围内,可见,那我就分发。
接下来详细看一下ViewGroup#dispatchTransformedTouchEvent
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled; // 是否消费
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction(); // 获取当前事件
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { // 取消,或者是取消事件
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) { // 传进来的子View为空
handled = super.dispatchTouchEvent(event); // 当前ViewGroup 来执行,调用的是父类View的方法
} else {
handled = child.dispatchTouchEvent(event); // 直接交给传进来的子View,在这里就是循环的时候倒序获取的View
}
event.setAction(oldAction); // 设置为 ACTION_CANCEL
return handled;
}
// Calculate the number of pointers to deliver.计算要传递的指针数。
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) { // 异常情况,放弃处理
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
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);
}
// Perform any necessary transformations and dispatch.
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(); // 回收TouchTarget
return handled;
}
这里引用大神的分析:
在dispatchTouchEvent()中多次调用了dispatchTransformedTouchEvent()方法,而且有时候第三个参数为null,有时又不是,他们到底有啥区别呢?这段源码中很明显展示了结果。在dispatchTransformedTouchEvent()
源码中可以发现多次对于child是否为null的判断,并且均做出如下类似的操作。其中,当child == null
时会将Touch事件传递给该ViewGroup自身的dispatchTouchEvent()处理,即super.dispatchTouchEvent(event)
(也就是View的这个方法,因为ViewGroup的父类是View);当child != null时会调用该子view(当然该view可能是一个View也可能是一个ViewGroup)的dispatchTouchEvent(event)处理,即child.dispatchTouchEvent(event)
。别的代码几乎没啥需要具体注意分析的。
具体的什么时候会传空呢,我们接着往下看,后面会分析和总结。
注释7
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); // 获取节点,并将数据装进去
target.next = mFirstTouchTarget; // 将新节点的next指向下一个节点
mFirstTouchTarget = target; // 头结点记录为当前节点
return target; // 返回头节点
}
到这里,整个 ViewGroup 层就结束啦,这里来总结下,dispatchTransformedTouchEvent()
什么时候会传入一个null的child呢?
- ViewGroup 没有子View
- 子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了false,这一般都是因为子 View 在onTouchEvent 中返回了 false。
注释8
这里主要分析的是循环中的链表操作
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;
}
View 最后可能接收到进行消费
我们知道前面按着正常情况下,就是调用View的dispatchTouchEvent方法,将事件传递给子View,接下来就是View的show time。
View#dispatchTouchEvent
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
* 传递给目标View 或者 查看它是否是目标
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
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)) { // 根据参数确定是否可以分发:这是一种安全策略(正常情况况下为true)
if ((mViewFlags & ENABLED_MASK) == ENABLED &&
handleScrollBarDragging(event)) { // 作为滚动条拖动就直接处理滚动事件,并直接消费,返回true
result = true; // 滚动条的时候
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo; // 各种listener定义在一起的静态内部类,包括我们熟悉的 onClickListener
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED // 验证 li 中的 mOnTouchListener 不为空,可以调用
&& li.mOnTouchListener.onTouch(this, event)) { // 调用onTouch 方法
result = true; // onTouch返回true就消费
}
if (!result && onTouchEvent(event)) { // onTouch 不消费就交给onTouchEvent,消费就变true
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;
}
看着注释基本都可以看懂,但是这里又一个东西得看一下,方便对一些事件的理解,那就是 onTouchEvent 方法:
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX(); // 获取点击坐标
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction(); // 获取Action类型
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 是否是可点击状态
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP: // 抬起的时候
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp(); // 处理弹窗类型的抬起事件
}
if (!clickable) { // 如果不可点击,移除相关接口设置和设置不可点击,并跳出选择
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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)) { // post到主线程执行这个Runnable,这Runnable是由View实现,内部调用li.mOnClickListener.onClick(this);
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: // 按下状态
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) { // 不是点击的话,有可能就是长按
checkForLongClick(0, x, y);
break;
}
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, x, y); // 为长按做准备
}
break;
case MotionEvent.ACTION_CANCEL: // 恢复默认状态
if (clickable) {
setPressed(false); // 恢复默认背景风格
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
所有的流程最后都可以归结到这张图上
事件分发拦截机制-详细图解整个事件传递就这样结束了,在这个过程中,拦截分发的代码交错在一起,我这里总结一下流程:
-
事件分发开始于Activity#dispatchTouchEvent,先交给getWindow().superDispatchTouchEvent(ev),返回false再交给Activity#onTouchEvent(ev)
-
在 PhoneWindow()#superDispatchTouchEvent(ev) 中,直接交给了顶层View:DecorView#superDispatchTouchEvent
-
在 DecorView#superDispatchTouchEvent 直接 super.dispatchTouchEvent(event),意味着调用父类ViewGroup#dispatchTouchEvent 处理。
-
调用 ViewGroup#onInterceptTouchEvent 判断是否拦截
如果拦截,就super.
如果不拦截并且是事件流的开始的话(DOWN 事件),就调用ViewGroup#dispatchTransformedTouchEven 分发下去
如果分发成功,就将分发成功的View存在 mFirstTouchTarget 链表中
如果遍历分发,没人消费,或没有子View的话,就调用父类(也是View啊)的 dispatchTouchEvent,这里面就会执行onTouch / onTouchEvent 方法
网友评论