这篇文章上次修改于 2341 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
Android 事件分发是安卓中一个很精妙的知识点,很多开发人员有些不解,为什么呢?因为不知道怎么去运用,其实这个地方理解起来比较简单,我写这么一篇文章就是为了串一串这个知识点。
脱离源码浅谈分发
- 事件分发是在最上层 View 一直往下层传递事件,并通过 onTouchEvent(MotionEvent ev) 这个方法来分发的,如果某个 View 想处理这个事件就需要重写这个 onTouchEvent() 并且返回 True ,然后写自己的逻辑处理事件。
- 如果是 ViewGroup 想处理这个事件呢,那么他就要重写 onInterceptTouchEvent(MotionEvent ev) 方法并返回 True ,来拦截这个事件并交给 ViewGroup 的 onTouchEvent() 来处理这个事件。
- 如果 ViewGroup 里面的子 View 想处理这个事件怎么办,不要担心 Android 给我们提供了 requestDisallowInterceptTouchEvent(boolean b) 方法然后传入 True 就会阻止上一级 ViewGroup 拦截事件,然后子 View 自己来处理这个事件。
- 上面三条结论我们怎么得出的呢?这就是 dispatchTouchEvent(MotionEvent ev) 方法的具体实现
处理 ACTION_DOWN 事件的 View 就是事件分发的目标,剩余的事件会直接分发到当前 View
滑动冲突有两种办法
- 如果两个上下级 View 冲突,那么给上级 View 设置 setOnTouchListener() 然后先把 ACTION_DOWN 分发给子 View ,然后在 ACTION_MOVE 中去判断具体的业务逻辑,确定到底那个 View 去处理本次事件。根据业务决定返回 True,False。
- 如果两个上下级 View 冲突,那么在下级 View 重写 onTouchEvent() ,然后先把 ACTION_DOWN 用 requestDisallowInterceptTouchEvent(true) 把上层事件处理拦截掉 ,然后在 ACTION_MOVE 中去判断具体的业务逻辑,确定那个 View 去处理本次事件,如果确定是上层 View 处理事件,则用 requestDisallowInterceptTouchEvent(false) 让上层 View 继续处理事件。
从源码看待事件分发
本次源码基于 Android 8.0 API 27
首先我们要知道事件分发的事件先发到 window 然后调用 Activity 或 Dialog 等组件的事件分发,然后分发到 window 再分发到 DecorView,再往我们自己的布局分发,这个流程我们将到 window 解析的时候讲,我们现在就当他是在 Activity 直接分发到我们自己的布局
1. 我们先从 ViewGroup 的 dispatchTouchEvent() 方法来说起
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
//检查点击是否合法
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
//如果之前有未处理完的事件则取消掉,具体实现看方法里面的源码,本质是往下分发 ACTION_CANCEL 事件
cancelAndClearTouchTargets(ev);
//清除 requestDisallowInterceptTouchEvent() 方法的标志,也就是每次 ACTION_DOWN 的时候这个标志都会取消
resetTouchState();
}
//检查是否拦截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果 onInterceptTouchEvent 返回 True 则拦截事件,系统控件一般情况都是 False
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
//没有触摸目标默认为拦截掉事件
intercepted = true;
}
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 检查是否取消事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//没有取消没有拦截
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//查找点击的 View
for (int i = childrenCount - 1; i >= 0; i--) {
...
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
...
//把新找到的触摸对象放到链表里
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
//没有找到接受事件的 View,交给最近处理事件的 View
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
if (mFirstTouchTarget == null) {
//没有触摸目标
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//寻找到或已经有触摸目标
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//分发事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
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;
}
mFirstTouchTarget 是记录点击的一个链表
我们再看一下分发事件的方法 dispatchTransformedTouchEvent()
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;
}
...
//继续放下分发
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
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) {
//如果没有子 View 则调用 View 的 dispatchTouchEvent() 方法,注意 View 和 ViewGroup 的 dispatchTouchEvent() 不一样,具体看下面
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
//往下分发,调用子 View/ViewGroup 的 dispatchTouchEvent() 方法
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
欧克,到此为止 ViewGroup 的两个主要的方法就差不多分析完了,我感觉我的注释打的也很全,有人会问了为什么没有 onTouchEvent() 方法,因为 ViewGroup 没有这个方法,等会我们分析 View 会将这个方法。
2. 接下来我们再看一下 View 的 dispatchTouchEvent() 方法
public boolean dispatchTouchEvent(MotionEvent event) {
...
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 如果开始 ACTION_DOWN 事件则取消当前的滑动
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//这个对象的作用就如他的名字,存储各种监听接口
ListenerInfo li = mListenerInfo;
//首先 ListenerInfo 不为空(则里面有监听对象),并且有 TouchListener 不为空,
//并且控件的状态是启用的,
//并且 TouchListener 的 onTouch 返回 true 说明事件被外面的 TouchListener 处理了,
//这就说明了外面设置监听接口优先级高于自己本身的 onTouchEvent() 方法,
//这就是我们解决滑动冲突第一种方法的依据
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果上述没有处理事件,则调用自身的 onTouchEvent() 方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
//如果手指抬起、时间取消、手指按下并由本 View 处理了事件,则停止 View 的滑动
stopNestedScroll();
}
return result;
}
接下来我们再看一看我们心心念念的 onTouchEvent() 方法
public boolean onTouchEvent(MotionEvent event) {
...
//判断事件点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//如果当前 View 没有启用则不处理该事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
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) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
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)) {
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;
}
//能否在容器内滚动
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
...
//清除各种状态
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
if (!pointInView(x, y, mTouchSlop)) {
//如果手指移动到 View 的外部则停止长按等事件
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
我们事件分发的内容基本也就这么多了,其实光讲事件分发的话 onTouchEvent() 这个方法将的有点多余了,其实与我们当前内容也没有多大的关系。
没有评论