11

Android 触摸事件分发机制(三)View触摸事件分发机制

 3 years ago
source link: https://www.viseator.com/2017/11/02/android_view_event_3/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

View触摸事件分发

经过前面的两篇文章,我们终于从内核(触摸事件的真正来源)一路经过Native层通过消息机制来到了需要接收的应用的主线程消息队列中然后被处理,首先调用的是应用根ViewDecorView)的dispatchPointerEvent()方法(继承自View):

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

调用了ViewGroupdispatchTouchEvent()方法(DecorView继承自FrameLayout):

dispatchTouchEvent(ViewGroup)

顾名思义,这个方法就是ViewGroup的触摸事件分发方法,它重写了父类View的该方法,View也有自己的dispatchTouchEvent()方法(后面再讲)。

这个方法非常长,我们拆开来分析,首先我们要明确一点,由于Android在系统级别引入了辅助功能选项(AccessibilityFoucs)来帮助有障碍的用户使用系统,所以如果一个事件带有TargetAccessibilityFocus标志,说明这是一个特殊的辅助功能事件,需要进行特殊处理(虽然这种情况比较少见)。

  @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 如果是辅助功能事件,并且当前view是目标view,那么取消标志,进行普通分发
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}

boolean handled = false;
// 安全原因检查触摸事件
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 如果事件类型是按下,清除之前的处理,重新开始处理触摸动作
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 拦截标志(重要)
final boolean intercepted;
// 如果是按下事件(新的触摸动作),或者已经存在处理事件的子View
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 检查是否不允许拦截事件(requestDisallowInterceptTouchEvent(true)被调用的情况)
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 调用onInterceptTouchEvent方法确定是否拦截事件(后面讲)
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 恢复action状态以避免其在上一行中被改变
} else {
// 不拦截(不允许)
intercepted = false;
}
} else {
// 如果不是一个新触摸动作的开始(不是down),并且没有处理该消息的目标(mFirstTouchTarget为null),说明当前view应该负责处理该事件,则当前view应该继续拦截并处理这个事件
intercepted = true;
}
// 如果被当前view拦截,或者已经有处理该事件的目标,则去除辅助功能标志,进行普通的事件分发
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}

这一段就是检测该view是否应该被拦截,虽然没有看下面的代码,我们可以猜测如果intercepted标志为true,那么这个事件就会留在该view被处理而不会再向其子view分发。下面是ViewGroup默认的处理方式:

onInterceptTouchEvent()

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

这个方法默认只是对一个特殊情况作了特殊的拦截处理。

dispatchTouchEvent(ViewGroup)

继续向下:

// 检查是否为取消事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// 分离触摸事件标志,如果是多点触摸,分别分发给多个view
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 如果未被取消并且没有被当前view拦截,应该进行向下分发
if (!canceled && !intercepted) {

// 如果是辅助功能事件,我们调用findChildWithAccessibilityFocus()来找到接收该事件的目标view
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
// 如果是一个按下事件(初始事件)
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // 按下事件为0
// 获取触摸点对应的PointerId,一个id表示一个触摸点,如果不分离,则获取全部的id
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;

// 清除之前的id信息
removePointersFromTouchTargets(idBitsToAssign);

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 创建待遍历的view列表,调用了buildTouchDispatchChildList()方法(见下文)
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
// 是否采用自定义view顺序(这个顺序将决定哪个view会先接收到事件)
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;

这段主要为后面的查找目标view作准备,我们先建立了preorderedList列表,我们来看看这个列表的顺序是如何构建的:

构建待遍历的view数组

public ArrayList<View> buildTouchDispatchChildList() {
return buildOrderedChildList();
}
ArrayList<View> buildOrderedChildList() {
final int childrenCount = mChildrenCount;
// 如果子view数小于等于1,或没有子view有z轴,直接返回null
if (childrenCount <= 1 || !hasChildWithZ()) return null;

if (mPreSortedChildren == null) {
mPreSortedChildren = new ArrayList<>(childrenCount);
} else {
mPreSortedChildren.clear();
mPreSortedChildren.ensureCapacity(childrenCount);
}
// 如果自定义绘制顺序,则应使用自定义分发顺序
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
// 获取正确的子view索引(不为自定义顺序时为i,自定义顺序时为自定义顺序对应的索引
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View nextChild = mChildren[childIndex];
// 保存z值
final float currentZ = nextChild.getZ();

// 如果列表中最后一个view的z值大于待插入的view,将当前view插入其之前,保证在后面从后向前遍历view时可以保存在屏幕最上面的view可以先接收到触摸事件
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}

总的来说,就是如果有自定义绘制顺序,那么按自定义绘制顺序,否则按默认绘制顺序,然后如果view定义了z值属性,那么在屏幕最上层的view应该先接收到触摸事件。

dispatchTouchEvent(ViewGroup)

回到分发方法,继续向下:

				for (int i = childrenCount - 1; i >= 0; i--) {
// 从preorderList中获取到正确的索引与子view
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);

// 如果需要处理辅助功能事件(找到了目标子view) if (childWithAccessibilityFocus != null) {
// 保证该子view最先接收到事件
if (childWithAccessibilityFocus != child) {
continue;
}
// 找到该子view后清除目标的记录
childWithAccessibilityFocus = null;
// 如果该子view没有处理辅助功能事件,那么应该重新遍历view进行普通分发,故将i重置
i = childrenCount - 1;
}
// 两个方法分别检查子view是否能接收触摸事件 与 触摸事件在该view的范围内,如果都成立,说明找到了应该处理该事件的子view
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

// 找到了目标子view,检查touchTarget链表中是否已经存在这个view
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 链表中已经存在该view,说明该子view已经接收过按下(初始)的触摸事件,说明这是一个多点触摸的情况,把新的点加入touchTarget并退出子view遍历
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}

resetCancelNextUpFlag(child);
// 在dispatchTransformedTouchEvent()方法中进行下一层的事件分发,如果该方法返回true,说明事件被后续的view处理了
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 保存mLastTouchDownTime、mLastTouchDownIndex、mLastTouchDownX、mLastTouchDownY
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 保存该view到touchTarget链表
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break; // 退出对子view的遍历,一个事件只会被分发给一个子view
}
ev.setTargetAccessibilityFocus(false);
} // 这里是遍历view循环结束点
// 清除preorderedList,避免view泄露
if (preorderedList != null) preorderedList.clear();
} // 这里if (newTouchTarget == null && childrenCount != 0) 判断结束点

首先明确一点,这段代码整个都是在

if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

这个条件下的,也就是说这是一个down事件,标志着一个新触摸动作的开始(一个触摸动作一般是down->move->up这样的顺序)。

我们在这段代码中找到了目标view,然后进一步调用dispatchTransformedTouchEvent()方法继续向下分发,如果该方法返回true,那么说明下面的子view处理了该事件,所以我们将该view保存到touchTarget链表中,然后保存了一些用于后续判断的事件信息。来看几个这段代码中调用的方法:

canViewReceivePointerEvents()

private static boolean canViewReceivePointerEvents(@NonNull View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}

检查是否可见,或存在动画。

isTransformedTouchPointInView()

protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
public void transformPointToViewLocal(float[] point, View child) {
point[0] += mScrollX - child.mLeft;
point[1] += mScrollY - child.mTop;

if (!child.hasIdentityMatrix()) {
child.getInverseMatrix().mapPoints(point);
}
}

将点值从屏幕坐标系转换到view的坐标系,然后检查是否在view的区域内。

dispatchTransformedTouchEvent()

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;

// 保存原始action,便于从调用改变`event`状态后恢复
final int oldAction = event.getAction();
// 取消事件的情况比较特殊,我们不用做任何过滤或转换
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
// 如果没有子view作为分发目标,则调用super(View类)的分发方法
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
// 向子view进一步分发
handled = child.dispatchTouchEvent(event);
}
// 恢复event状态
event.setAction(oldAction);
return handled;
}

// 过滤出需要的触摸点id
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

if (newPointerIdBits == 0) {
return false;
}

final MotionEvent transformedEvent;
// 如果所有触摸点都被使用,我们可以直接使用原event,如果不是,我们需要从中分离出一个新的transformedEvent副本再进行分发,这么做的是因为我们要保持原event的状态
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;
// 计算并设置view偏移给event
event.offsetLocation(offsetX, offsetY);
// 分发事件
handled = child.dispatchTouchEvent(event);
// 恢复event原状态
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
// 复制一个event
transformedEvent = MotionEvent.obtain(event);
} else {
// 分离出的新的transformedEvent
transformedEvent = event.split(newPointerIdBits);
}

if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}

handled = child.dispatchTouchEvent(transformedEvent);
}
// 回收临时拷贝
transformedEvent.recycle();
return handled;
}

这个方法中对触摸事件设置好正确的偏移后向目标子view进行分发,如果没有目标,则调用自身的父类,也就是view的分发方法进行处理。

addTouchTarget()

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}

这里mFirstTouchTartget是链表头,新增的touchTarget被插入了表头位置。

dispatchTouchEvent(ViewGroup)

再回到这个主要方法中:

// 没找到可以接收事件的子view
if (newTouchTarget == null && mFirstTouchTarget != null) {
// mFirstTouchTarget为链表头
newTouchTarget = mFirstTouchTarget;
// 把newTouchTarget指向表尾
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}

注意11、12行的两个右括号分别对应退出的是

if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

这意味着我们已经处理完了没有被取消、当前ViewGroup拦截,并且为初始触摸事件(Down) 的情况的分发,但是要注意的是现在并没有退出函数,还要继续向下执行:

      if (mFirstTouchTarget == null) {
// 意味着还没有子view处理该触摸事件
// 此时第三个参数为null,会向父亲View传递令其处理这个事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 说明已有子view处理过该事件序列(由`down`开始),直接将事件分发给该view,当需要时取消事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// 该TouchTarget已经在前面的情况中被分发处理了,避免重复处理
handled = true;
} else {
// 如果被当前ViewGroup拦截,向下分发cancel事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// dispatchTransformedTouchEvent()方法成功向下分发取消事件或分发正常事件
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;
}
}
        if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 如果为up事件或者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;

dispatchTouchEvent(ViewGroup)小结

现在我们再从头梳理一遍这个比较长的方法过程:

  • TouchTarget链表保存了处理了初始触摸事件的子View,注意只有一系列触摸动作的初始事件(Down事件)才会找到对应的子View并生成TouchTarget的一个节点。后面的系列事件都会分发给TouchTarget链表中保存的子View,这也就意味着,如果一个子View没有处理初始的Down事件,那么它也就不会再接收到后面的move up等事件。
  • 如果onInterceptTouchEvent()返回true,当前ViewGroup拦截了该事件,那么该事件不会再向下面分发,并且会向TouchTarget中保存的所有子View发送cancel事件提醒它们这一系列的事件已经因被拦截而取消了,同时还会移除分发记录,意味着后面的事件也不再会分发到子View
  • 如果是辅助功能的事件,那么会优先分发给支持辅助功能的View,如果不存在这样的view,则进行一般的事件分发。

顺序(大致):

  1. 判断是否被拦截
  2. 如果未被拦截且为初始事件,找到可以处理事件的子View(在点击范围内且可被点击),分发事件后如果该子View处理了事件(dispatchTouchEvent()方法返回true)则存入TouchTarget链表并停止子View的遍历(后面的子View就没有机会再收到事件),如果该子View没有处理该事件,则继续遍历寻找
  3. 如果事件被拦截,向TouchTarget中的子View发送cancel事件
  4. 将未被2、3情况处理的事件分发给TouchTarget中的子View,如果TouchTarget为空,则交给ViewGroup本身父ViewdispatchTouchEvent()方法处理

dispatchTouchEvent(View)

现在我们知道,当一个触摸事件分发到一个非ViewGroupView或者ViewGroup不再向下分发该事件(没有处理事件的目标或者被本身拦截),那么View类的dispatchTouchEvent()将会被调用:

public boolean dispatchTouchEvent(MotionEvent event) {
// 处理辅助功能事件的情况
if (event.isTargetAccessibilityFocus()) {
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
event.setTargetAccessibilityFocus(false);
}

boolean result = false;
// 一致性检验,检查事件是否被改变
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 停止滚动(如果在)
stopNestedScroll();
}

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
// 如果事件为鼠标拖动滚动条
result = true;
}

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
// 如果注册了点击事件监听(onTouchListener),并且当前view处于启动状态,并且调用注册的onTouch方法返回了true,说明事件被消耗,标记结果为true,(注意这个时候已经调用了`onTouch()`进行事件分发处理
result = true;
}
// 如果没有被注册的onTouch方法消耗事件,那么调用View本身的onTouch方法,如果返回了true,说明事件被消耗,标记结果为true
if (!result && onTouchEvent(event)) {
result = true;
}
}
// 一致性检验,检查事件是否被改变
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}

// 如果是up事件(系列触摸动作的终点),或者是cancel事件,或者是初始事件并且我们没对它进行处理(回忆前面的内容,如果没有处理down事件,那么也不会收到后面的事件),就停止滚动状态
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}

return result;
}

我们可以看到,ViewonDispatchTouchEvent()方法主要是先检查是否注册了onTouchListener,如果注册了监听并且调用返回了true消耗了该事件,那么说明该View处理了该事件,也会收到后续的事件,如果没有注册监听或者没有消耗,就调用View本身的onTouchEvent方法,如果返回true则消耗事件。

下面来看View默认的onTouchEvent()方法:

onTouchEvent()

这个方法我们也拆开来看:

public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();

if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
// 如果view被禁用且按下状态为true,取消接下状态
setPressed(false);
}
// 如果该view被禁用,但是被设置为clickable或longClickable或contextClickable,仍然消耗该事件
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
// 如果为该view设置了触摸事件代理,则转发到代理处理触摸事件
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

处理了view被禁用和设置了触摸事件代理的情况。

if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {

注意下面的语句都是在该view可被点击的情况下执行的,并且一旦该判断成立,那么最终一定会返回true,也就是说,设置了可被点击的view在默认情况下一定会消耗触摸事件。

下面对不同的触摸事件类型分别作出处理,为了分析方便,我调换了各case的顺序:

case MotionEvent.ACTION_DOWN:

一个触摸动作的开始

mHasPerformedLongPress = false;
// 检查Button点击事件的特殊情况(下文讲)
if (performButtonActionOnTouchDown(event)) {
break;
}
// 向上遍历view以检查是否处在一个可滚动的容器中
boolean isInScrollingContainer = isInScrollingContainer();

// 如果是在滚动容器中,稍延迟触摸反馈来应对这是一个滚动操作的情况
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
 // 新建一个对象用于检测单击事件(下文讲)
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
 // 利用消息队列来延迟发送检测单击事件的方法,延迟时间为getTapTimeout设置的超时(下文讲)
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// 没有在滚动容器中,马上显示触摸反馈,并且开始检查长按事件(下文讲)
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
performButtonActionOnTouchDown()
protected boolean performButtonActionOnTouchDown(MotionEvent event) {
if (event.isFromSource(InputDevice.SOURCE_MOUSE) &&
(event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
showContextMenu(event.getX(), event.getY());
mPrivateFlags |= PFLAG_CANCEL_NEXT_UP_EVENT;
return true;
}
return false;
}

只是处理了事件来源是鼠标的特殊情况。

CheckForTap()

它是一个Runnable,用于延迟执行单击检测的任务:

private final class CheckForTap implements Runnable {
public float x;
public float y;

@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}

它被放到消息队列,在设置的超时之后被执行,如果这段时间它没有被移出队列,那么说明这就是一个单击事件,那么就显示触摸反馈并开始长按检测。

postDelayed()
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}

getRunQueue().postDelayed(action, delayMillis);
return true;
}

往注册的Handler的消息队列或者他自己实现的一个消息队列中发送需要被延时执行的消息,这块就不深入探究了,消息机制分析的文章已经讲得很清楚了。

checkForLongClick()
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;

if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}

同样是利用postDelayed()方法来检测是否到达了检测时间,

private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;

@Override
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}

public void setAnchor(float x, float y) {
mX = x;
mY = y;
}

public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}

如果run()方法被执行,说明到达了设定的时间并且没有因为触摸点移动或者抬起而移除该Runnable信息,为一个长按动作,执行performLongClick()方法来触发长按回调:

public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
public boolean performLongClick() {
return performLongClickInternal(mLongClickX, mLongClickY);
}
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}

第5-8行是与之前onTouchEvent()方法中类似的关于是否注册了监听器的判断,如果注册了监听器,那么优先使用监听器的onLongClick()方法来处理长近事件,如果没有监听器成功处理事件,那么会先判断长按是否有锚点,再根据锚点的存在性调用showContextMenu()显示可能存在的上下文菜单。

13行判断如果当前方法成功消耗了长按事件,调用performHapticFeedback()方法显示一个触觉的反馈。

case MotionEvent.ACTION_MOVE:

触摸点发生了移动

drawableHotspotChanged(x, y);

if (!pointInView(x, y, mTouchSlop)) {
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
removeLongPressCallback();

setPressed(false);
}
}
break;

第1行调用了drawableHotspotChanged()通知可能存在的子Viewdrawable触摸点发生了移动。

第3行检测由于移动,触摸点是否移出了view+slop扩展出的范围,slop的存在是为了保证在按下后轻微移出点击区域的情况下能正常判断点击:

public boolean pointInView(float localX, float localY, float slop) {
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}

如果移出了这个范围,首先第4行调用removeTapCall()

private void removeTapCallback() {
if (mPendingCheckForTap != null) {
mPrivateFlags &= ~PFLAG_PREPRESSED;
removeCallbacks(mPendingCheckForTap);
}
}

先取消了预按下状态的flag,再调用removeCallbacks

public boolean removeCallbacks(Runnable action) {
if (action != null) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mHandler.removeCallbacks(action);
attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
Choreographer.CALLBACK_ANIMATION, action, null);
}
getRunQueue().removeCallbacks(action);
}
return true;
}

从消息队列中移出我们检测单击事件的消息,这样,由于触摸点移动出了当前view,如果在滚动容器中的情况下,长按的检测就不会进行(因mPendingCheckForTap消息被移出消息队列)。

if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
removeLongPressCallback();

setPressed(false);
}

如果pressed标志位为1,那么就取消消息队列中长按触发消息,同时去除pressed标志位。

总结一下,只要触摸点移动出了当前view,那么所有的点击、长按事件都不会触发,但是只要移动还在view+slot范围内,那么点击长按事件还是会被触发的。

case MotionEvent.ACTION_UP:

boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 如果有prepressed或pressed标志
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
// 可以获得焦点但没有获得
// 请求获取焦点
focusTaken = requestFocus();
}

if (prepressed) {
// prepressed状态表示滚动容器中的点击检测还没有被消息队列执行,这个时候如果抬起手指说明是一个点击事件,调用setPressed显示反馈
setPressed(true, x, y);
}

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 没有到达执行长按触发消息的时间就抬起了手指,说明这是一个单击事件,移除长按触发消息
removeLongPressCallback();

if (!focusTaken) {
// 当当前view没有获取焦点时才能触发点击事件,说明一个可以获取焦点的view是无法触发点击事件的
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// 使用post来将performClick动作放入队列中执行来保证其他view视觉上的变化可以在点击事件被触发之前被看到
if (!post(mPerformClick)) {
// 如果post没有成功,则直接执行
performClick();
}
}
}
// UnsetPressedState为Runnable消息,用于取消view的prepressed或pressed状态
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}

if (prepressed) {
// 取消prepressed状态
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// 取消pressed状态
mUnsetPressedState.run();
}
// 清除单击检测消息
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;

可以看到up时,才是单击事件真正触发的地方,如果这个view可以获得焦点,那么会优先处理焦点获取,而不会触发点击事件。

public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}

相似的方法,检测了是否有监听的存在并执行,最后给辅助功能选项发送一条消息。

case MotionEvent.ACTION_CANCEL:

取消这一系列触摸动作

case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;

清除所有的状态。

View默认的onTouchEvent()方法处理了一系列的触摸事件, 判断是否触发单击、长按等,并且提供了默认的按下、点击、长按的视觉反馈。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK