前言
View的继承关系
Android 中的 View 的继承关系,View 用来处理事件,ViewGroup 用来分发事件;
事件的分发流程如下:
这个在上一章(嵌套滚动大揭秘)中有简单介绍;
View 的事件处理流程
onTouch 如何消费?onClick 如何消费?
我们可以先来看一个小 demo
typescript
mTvJustWatchTitle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//
}
});
mTvJustWatchTitle.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
当我们的 onTouch 方法 return false 的时候,我们发现,onClick 方法执行了,当 return true 的时候,onClick 方法却没有执行,那么这是为什么呢? onClick 为什么会与 onTouch 发生了冲突呢?
什么是事件冲突?
事件只有一个,多个人想要处理,如果处理的对象不是我们想要的对象,那么就被称为事件冲突!
我们进入 View 的 dispatchTouchEvent 方法看下,事件是怎么消费的,谁的优先级较高
csharp
public boolean dispatchTouchEvent(MotionEvent event) {
// 省略部分代码
...
//
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
// 因为我们调用了 setOnTouchEventListener 并实现了这个方法,那么这个mListenerInfo就一定不为空了,
//
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;
}
} // 省略部分代码
...
//
}
也就是说,如果 onTouch 方法中 return true 的时候,这个 result = true 就会成立;如果 return false,这个 result = true 就不会成立,当 onTouch return false 的时候,我们接着进入 onTouchEvent 方法看下,可以看到,这个方法里面我们进行了事件的处理操作,里面实现了 MotionEvent 的 UP、DOWN、MOVE、CANCEL 等事件;
typescript
public boolean onTouchEvent(MotionEvent event) {
//
...
// 省略部分代码
switch (action) {
case MotionEvent.ACTION_UP:
//
...
// 省略部分代码
// 可以看到,onClick 的执行,是在 UP 事件的 performClickInternal 方法中
if (!post(mPerformClick)) {
performClickInternal();
}
break;
}
}
我们进入 performClickInternal 方法看下:
scss
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
方法调用了 performClick 方法,我们进入这个方法看一下:
java
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 可以看到,最终调用了我们实现的 onClick 方法;
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
看到这里,我们也就能理解了,为什么 onTouch 方法 return false 的时候,onClick 才会执行的原因,因为 onTouch 方法先执行,如果 return true,则不会执行 onClick 方法了;也就是我们需要根据业务需求,设置不同的处理对象,来防止事件处理冲突;
View 的事件分发流程
ACTION_DOWN 事件分发流程
我们前面有提到,事件的分发是在 ViewGroup 中进行的,我们进入 ViewGroup 的 dispatchTouchEvent 方法看一下:
我们以 ViewPager 嵌套 RecyclerView 为例子:
如果我们重写了 ViewPager 的 onInterceptTouchEvent 方法,并 return true,则表明由 ViewPager 拦截此次事件;
java
public boolean dispatchTouchEvent(MotionEvent ev) {
// 省略部分代码
...
//
if (onFilterTouchEventForSecurity(ev)) {
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;
}
//
}
//
}
首先会通过 onInterceptTouchEvent 判断是不是要拦截,如果拦截这个事件,也就是 intercepted 就会等于 true;则会进入 dispatchTransformedTouchEvent
typescript
public boolean dispatchTouchEvent(MotionEvent ev) {
// 省略部分代
...
// 省略部分代码
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
// 省略部分代码
...
// 省略部分代码
}
我们进入这个 dispatchTransformedTouchEvent 方法看下:
ini
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// 省略部分代码
...
//
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);}
}
首次的时候,这个 child 为空,进入了super.dispatchTouchEvent(transformedEvent)这个方法,而 ViewGroup 的父类是 View,所以进入了 View 的 dispatchTouchEvent 方法,进行了事件的处理逻辑;
如果 onInterceptTouchEvent return true,则这个 ViewGroup 消费当前事件的流程也就整体串联了起来,当前 ViewGroup 的后续 MOVE、UP、CANCEL 等就会在当前 ViewGroup 消费掉;
我们接着返回去看 不拦截 的 case:
scss
public boolean dispatchTouchEvent(MotionEvent ev) {
//
...
// 省略部分代码
if (!canceled && !intercepted) {
//
...
// 省略部分代码
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//
...
// 省略部分代码
if (newTouchTarget == null && childrenCount != 0) {
// 给当前 ViewGroup 的子 View 进行一个排序
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
}
}
}
}
不拦截,也就是 onInterceptTouchEvent return false,intercepted 就会等于 false,则先判断这个 ViewGroup 有没有子 View,并获取子 View 的个数,以及调用 buildTouchDispatchChildList 对子 View 进行排序,排序规则如下:
根据 xml 中声明的顺序,倒序插入到这个 ArrayList 中;
然后获取的时候,倒序取出,也就是最终还是先写的,先取出来 ;
css
for (int i = childrenCount - 1; i >= 0; i--) {}
倒序取出来之后,进行相关逻辑处理:
ini
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
}
取出 View 之后,会进行能不能接受事件处理的判断,第一个 canReceivePointerEvents 方法中首先会判断当前 View 是不是可见的,然后判断当前 View 是不是有动画;
第二个 isTransformedTouchPointInView 方法中判断触摸的地方是不是在当前 View 上;
然后调用 dispatchTransformedTouchEvent 进行事件的分发
scss
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 分发处理事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
}
}
这里会进行事件的分发 dispatchTransformedTouchEvent 由于我们前面已经将 child 获取到了,所以这个这个方法中的第三个参数 child 就不会为空;就会进入 dispatchTransformedTouchEvent 这个方法的 else 逻辑里面
ini
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
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);
}
}
这里 else 中,如果子 View 还是 ViewGroup,那么就会进入下一轮的递归调用,直到分发到 View 进行事件的消费;
如果 dispatchTransformedTouchEvent 返回 false,则开始下一轮的循环;
如果 dispatchTransformedTouchEvent 返回 true,则会进入事件处理流程,同时 break 循环;
ini
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 = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
最终会调用 addTouchTarget 将当前 View 加入到一个 View 调用链中,并将 mFirstTouchTarget 赋值;
ini
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
经过 addTouchTarget 之后,target.next = null;mFirstTouchTarget = target;mTouchTarget = target;
也就是 mFirstTouchTarget == mTouchTarget != null;循环终止后,接着进入下面不拦截的流程,也就是 else 流程
ini
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;
}
}
predecessor = target;
target = next;
}
}
target != null 由 target.next = null 并且 final TouchTarget next = target.next 可以看到,while 循环只会执行一次;
又因为 alreadyDispatchedToNewTouchTarget && target == newTouchTarget 成立,所以 handled = true;
else 的逻辑,是针对的多点触控的逻辑,不在本次分析范围内,感兴趣的可以自己看下;
如果所有的子 View 都不处理,则相当于是自身进行了拦截,就会进入拦截处理的逻辑;
ini
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
ACTION_MOVE 事件分发流程
Move 事件也分拦截和不拦截的情况;
我们先看不拦截的情况,来看下 MOVE 事件是怎么分发的,同样,我们进入 dispatchTouchEvent 方法看下:
其实可以看到,两个 if 都进行了 DOWN 的判断,所以 MOVE 事件不会进入到这里;
所以,逻辑直接就进入到了 else 的 while 循环里面,最终进入的就是 dispatchTransformedTouchEvent
可以看到,MOVE 事件,不会进行子 View 的问询操作,而是直接分发到目标子 View;
我们来看下拦截的情况,MOVE 事件冲突处理;
事件冲突处理其实可以分为 内部拦截法 和 外部拦截法;
内部拦截法,子 View 处理滑动事件;外部拦截法,父 View 处理滑动事件;
内部拦截法
子 View 处理滑动事件的时候,需要让父 View 不进行事件的拦截,而父 View 在进行拦截之前会有一个判断:
ini
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;
}
}
也就是说,如果我们让 disallowIntercept = true 的话,那么,父 View 就不会拦截子 View 的事件,而设置这个的值,是通过 requestDisallowInterceptTouchEvent 来实现的;
typescript
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);
}
}
也就是说内部拦截法中,子 View 中在 DOWN 事件的时候调用 getParent().requestDisallowInterceptTouchEvent(true),请求父 View 不拦截事件;在 MOVE 事件的时候根据滑动方向,如果需要父 View 处理,则调用 getParent().requestDisallowInterceptTouchEvent(false),请求父 View 拦截事件;
举个例子,我们用 ViewPager 来嵌套一个 RecyclerView 来看下效果,也就是我们最终的代码实现如下:
ViewPager 实现如下:
less
public class BadViewPager extends ViewPager {
private int mLastX, mLastY;
public BadViewPager(@NonNull Context context) {
super(context);
}
public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
// 外部拦截法:父容器处理冲突
// 我想要把事件分发给谁就分发给谁
// ViewPager 进行事件的拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return true;
}
}
RecyclerView 实现如下:
java
public class MyRecyclerView extends RecyclerView {
public RecyclerView(Context context) { super(context);
}
public RecyclerView(Context context, AttributeSet attrs) { super(context, attrs);
}
// 内部拦截法:子view处理事件冲突
private int mLastX, mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 在 DOWN 的时候,要求 ViewPager 不拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
}
我们运行看下效果:
可以看到,并没有达到我们期望的效果,RecyclerView 并不能上下滑动,只能 ViewPager 的左右滑动,事件还是冲突了,那么问题出在哪里呢?我们接着分析 dispatchTouchEvent 源码;
我们发现,在源码中,有这么一段逻辑:
scss
// Handle an initial down.i
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();
}
如果是 DOWN 事件,会把所有的参数状态进行重置,包括了 mGroupFlags 也就是说,我们在 RecyclerView 中设置的 mGroupFlags 并没有生效,在 DWON 的时候被重置了;
也就是说,DOWN 事件中
ini
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
disallowIntercept 一定是 false;
ini
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
}
这段逻辑,一定走了进来,也就是 intercepted 是 true,事件进行了拦截,因为我们前面把 ViewPager 的 onInterceptTouchEvent 方法返回了 true;根据前面的分析,ViewPager 把 DOWN 事件进行了拦截,也就不会分发给子 View(RecyclerView)了,所以,RecyclerView 就能竖向滑动了,如果 RecyclerView 想拿到竖向滑动的事件,那么父 View(ViewPager)对 DWON 事件不能进行拦截,修改如下:
less
public class BadViewPager extends ViewPager {
private int mLastX, mLastY;
public BadViewPager(@NonNull Context context) {
super(context);
}
public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
// 外部拦截法:父容器处理冲突
// 我想要把事件分发给谁就分发给谁
// ViewPager 进行事件的拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// 对 DOWN 事件不进行拦截,交给子 View(RecyclerView) 处理;
if(event.getAction() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(event);
return false;
}
return true;
}
}
ViewPager 不拦截 MOVE 事件,当 RecyclerView 拿到 MOVE 事件的时候,进行左右滑动的时候,要求父 View(ViewPager)拦截事件自己处理,通过 getParent().requestDisallowInterceptTouchEvent(false);
ViewPager 拦截了 MOVE 事件,intercepted = true,我们看下 ViewPager 怎么处理 MOVE 事件:
ViewPager 执行到了 else 的逻辑中,因为 intercepted = true,所以 cancelChild 也等于 true;
diapatchTransformedTouchEvent 中 cancelChild 传入的就是 true,我们进入这个方法看下:
可以看到,子 View (RecyclerView)的 MOVE 事件被替换成取消了,然后调用子 View (RecyclerView)的 dispatchTouchEvent 方法进行事件的处理,RecyclerView 最终调用的是 return super.dispatchTouchEvent(),所以 diapatchTransformedTouchEvent 最终返回的也是 true,所以 handled = true;
因为 cancelChild = true;所以 mFirstTouchTarget 被重置成了 null,前面有分析 next 其实空的原因;
那么当第二个 MOVE 事件进来的时候,父 View(ViewPager)直接进行拦截,intercepted = true;
接着进入了事件分发处理流程 dispatchTransformedTouchEvent 方法
接下来的 MOVE 事件,就被父 View(ViewPager)拦截并处理了;
我们运行看下效果:
我们看到,可以上下滑动了,RecyclerView 拿到了 MOVE 事件;
外部拦截法
外部拦截法就比较简单了,判断父 View(ViewPager)的滑动方向和触摸事件,进行处理即可,子 View(RecyclerView)不用做任何处理;
java
public class BadViewPager extends ViewPager {
private int mLastX, mLastY;
public BadViewPager(@NonNull Context context) {
super(context);
}
public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
// 外部拦截法:父容器处理冲突
// 我想要把事件分发给谁就分发给谁
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mLastX = (int) event.getX();
mLastY = (int) event.getY();
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
// 水平滑动的时候,自己处理,垂直滑动的时候,不处理
if (Math.abs(deltaX) > Math.abs(deltaY)) {
return true;
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
return super.onInterceptTouchEvent(event);
}
}
简历润色
简历上可写:深度理解事件分发的原理,可解决复杂滑动冲突;
下一章预告
自定义 ViewPager 炫酷指示器~
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~~感谢!