事件处理机制原理分析
根据 Android12(SDKVersion:32)源码进行分析的
View的继承关系
事件处理机制的几个重要方法
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
TouchEvent 事件传递
由日志文件可得 :
ViewGroup 容器: dispatchTouchEvent 是由最底层 向 上层 分派 事件的,分派到最上层时 调用 view的onTouchEvent事件进行处理(递归传递)。如果 dispatchTouchEvent函数 return true/false 事件就不会往上传递了,return super.dispatchTouchEvent(...) 才会往上传。
ViewGroup 也可以使用 onInterceptTouchEvent 事件拦截回调方法 return true,拦截后,就可以在 onTouchEvent 方法中对事件进行处理
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true; // 拦截,事件不会往上传递
// return false; //不拦截,事件往上传递
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 处理事件
}
View 的 dispatchTouchEvent、 onTouchEvent 都是从 最上层 往下 传递事件
所以 onTouchEvent 事件,是从 最上层 往下 传递事件
事件传递全流程-源码分析
带着以下问题看源码
1、onTouch 和 onClick 的关系 与 执行的位置 2、onTouchEvent 在哪儿执行的 3、LongClick 事件触发 4、按下移出 View,为什么 onClick 不执行?
dispatchTouchEvent@Activity.java
getWindow()@PhoneWindow.superDispatchTouchEvent(ev)
mDecor.superDispatchTouchEvent(event);
super.dispatchTouchEvent(event);
ViewGroup.dispatchTouchEvent // 事件分发机制
// 这里就能解释为什么 view 的事件传递是 从上往下传递
for (int i = childrenCount - 1; i >= 0; i--) {
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
child@View.dispatchTouchEvent //事件处理方法
// 如果设置了 OnTouchListener,而且返回true
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// result 为 true 就不会执行 onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
// onTouchEvent 在 child@View.dispatchTouchEvent 里面执行
onTouchEvent@View.java
MotionEvent.ACTION_DOWN: // 按下屏幕
checkForLongClick // 长按事件,延时操作400ms, 如果400ms内 抬起手或者移出到view外 长按事件就会被移除
MotionEvent.ACTION_UP: // 抬起手指,离开屏幕
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 查看标识是否能执行点击事件
removeLongPressCallback(); // 移除长按事件
performClickInternal();//onclick事件是在onTouchEvent事件里面处理的
li.mOnClickListener.onClick(this)
}
MotionEvent.ACTION_MOVE: // 手指在屏幕上移动
if (!pointInView(x, y, touchSlop)) {//表示 touch事件 移出了view
removeLongPressCallback(); // 移除长按事件
setPressed(false); // 设置标识,让点击事件失效
}
结论
- ViewGroup的事件分发是 从下往上分发的,View 是从上往下消费事件的
- 如果view 同时设置了 onTouch事件和 onClick事件,如果onTouch事件里返回了true,onClick事件、长按事件都不会执行。
同时设置 onTouch 和 onClick - 实战代码
tv.setOnTouchListener((v, event) -> {
Log.d("hehe", "onTouch: "+event.getAction());
// return true; // 不会执行onCLick
return false; // return false 且 没移动到view外面的话 会执行 onClick
});
tv.setOnClickListener(v -> {
Log.d("hehe", "OnClick: "+v.getAccessibilityClassName());
});
log:
dispatchTouchEvent 函数详解
父容器分发流程 》 递归 》 责任链模式
MotionEvent.ACTION_DOWN // 只执行一次,手指初次点击时执行
MotionEvent.ACTION_UP // 只执行一次,手指最后抬起时执行
MotionEvent.ACTION_POINTER_DOWN // 多指操作 按下标识
MotionEvent.ACTION_POINTER_UP // 多指抬起标识
第一根手指按下:ACTION_DOWN
第二根 - 第n-1根:ACTION_POINTER_DOWN
抬起:最后一根手指之前:ACTION_POINTER_UP
抬起:最后一根手指:ACTION_UP
源码分析
// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
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;
// 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();
}
// 检查是否拦截 -- 父容器的权利.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// disallowIntercept 决定了 onInterceptTouchEvent 会不会执行
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;
}
// 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.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
// 是否支持多指,默认为 true
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 在 if 中分发事件
if (!canceled && !intercepted) { // 没被取消和拦截,就分发事件
// If the event is targeting accessibility 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;
// 第一根手指按下
if (actionMasked == MotionEvent.ACTION_DOWN
// 第 n(n>1) 根手指按下
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
// 鼠标移动事件
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 表示手指按下顺序的下标,ACTION_DOWN , actionIndex = 0
final int actionIndex = ev.getActionIndex(); // always 0 for down
// 手指的id,用于识别是那根手指在操作 int(4个字节,32位),1位代表1根手指
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;
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
// 将子view 进行排序(按z轴 由低到高)
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 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 (!child.canReceivePointerEvents()
// 判断点击区域是否在 view上
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 单指操作为空,多指才不为空
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 询问 child 是否处理事件,如果child处理,则命中if -- 递归
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 创建了 newTouchTarget(链表结构) = mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 表示child 处理了事件
alreadyDispatchedToNewTouchTarget = true;
// 如果 dispatchTransformedTouchEvent return true,
// 表示child处理了事件,退出循环,不再访问其他child
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
// 没有child处理事件的时候
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 询问自己是否处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
// handled = super@View.dispatchTouchEvent(transformedEvent);
} 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;
// 表示 已经有child处理了事件,直接返回 handled,不作处理
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 询问 target.child 是否处理
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 如果 cancle为 true,取消child处理事件
if (cancelChild) {
if (predecessor == null) {
// mFirstTouchTarget 置为 null(如果是单指的情况)
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;
}
Android 最多识别多少手指?
// 手指的id,用于识别是那根手指在操作 int(4个字节,32位),1位代表1根手指 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
实例分析
自定义view 重叠时,冲突是必然的,就需要根据需求来处理冲突
ViewGroup才分发事件(最底层往上分发),View只消费事件(最上层往下消费)
处理冲突:1、内部拦截法(由子view根据条件 决定事件 由谁处理);2、外部拦截法(由父容器决定)
ViewPager + ListView的组合
默认情况下,只能上下滑动,ViewPager父容器是默认(onInterceptTouchEvent{return false})不拦截事件的,ListView接收到事件 会进行处理
如果 ViewPager 重写了 onInterceptTouchEvent 并且 return true 事件就不会分发到子view。UI情况就是 只能 ViewPager的左右滑动,不能上下滑动
问题:现在假设 ViewPager 除了 action_down 能分发事件之外,其他都被拦截了,在ListView 中使用内部拦截法来解决问题?
parent.requestDisallowInterceptTouchEvent(true); // 这个方法能使 parent 里面的 onInterceptTouchEvent 失效
所以内部拦截法解决问题的思路是:
- ViewPager 的action_down 分发事件给 ListView之后,
- ListView 在 action_down 事件里面 调用parent.requestDisallowInterceptTouchEvent(true); 使得ViewPager 的onInterceptTouchEvent 失效
- 在 ListView里面的 action_move 里面 按需求判断 是否让 ViewPager 拦截事件,以此解决事件冲突问题。
外部拦截法思路:
在ViewPager 的onInterceptTouchEvent 里面决定是否拦截事件,拦截的话就ViewPager自己处理事件(可以左右滑动),不拦截的话就分发事件给 ListView,ListView是默认处理事件的(可以上下滑动)