对于Android开发者来说,自定义View是必须攻克的一关,也是从初级工程师迈向高级的进阶关卡,要想通过此阶段,除了必须掌握View的测量、绘制、滑动等基础知识外,更要掌握View的核心知识点:View的事件分发,本篇就一起从源码的角度分析View和ViewGroup的事件分发机制;
在我们平时的使用或写自定义View时,都会直接或间接的使用View的事件分发,View的事件分发主要与View源码中的3个方法有关:
下面我们针对这三个方法从源码学习和分析事件的分发,一起从本质上掌握View是如何在层层传递和消耗事件;
public boolean dispatchTouchEvent(MotionEvent event) {
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;
}
}
上面代码是dispatchTouchEvent()中的部分代码,也是与我们使用最接近的核心代码,首先会判断View是否设置触摸监听mOnTouchListener,如果设置则会调用OnTouchListener.onTouch()方法,如果此方法返回true,则dispatchTouchEvent()返回true即拦截事件,若onTouch()返回false,则调用onTouchEvent(),如果onTouchEvent()返回true则事件被消耗,否则事件继续传递;从上面的方法和叙述我们可以得出以下结论:
1.1、onTouchEvent()源码分析
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN; // 设置mPrivateFlags3为FINGER_DOWN标记
}
mHasPerformedLongPress = false; //设置false表示此事还未出发长按事件
boolean isInScrollingContainer = isInScrollingContainer(); // 调用父容器的shouldDelayChildPressedState(),默认true
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED; // 状态设置为中间状态PFLAG_PREPRESSED
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); // 延时发送执行CheckForTap中的run(),ViewConfiguration.getTapTimeout() = 100ms
} else {
setPressed(true, x, y);
checkForLongClick(0, x, y); // 直接检测长按事件
}
//CheckForTap中调用检测长按事件
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);//调用长按检测方法
}
// checkForLongClick中延时发送CheckForLongPress实例
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset); // getLongPressTimeout()为 500ms(系统默认的长按时间)
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) { //
mHasPerformedLongPress = true; // 设置标志表示触发长按;此标志是否为true取决于li.mOnLongClickListener.onLongClick的返回值
}
}
}
//在performLongClick()中代码会最终调用performLongClickInternal()
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this); //调用长按监听中的onLongClick();返回值影响mHasPerformedLongPress
}
以上代码是View的onTouchEvent()的ACTION_DIOWN执行逻辑,只粘贴了部分关键代码,所执行逻辑如上面注释,下面我们逐步分析以下:
if (!pointInView(x, y, mTouchSlop)) { //判断手指是否划出View范围
removeTapCallback(); // 移除CheckForTap事件
removeLongPressCallback(); // 移除长按检测事件
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
在Action_Move事件中,主要根据手指滑动的坐标判断是否移除View的范围,若移除则取消和移除CheckForTap事件
if (!clickable) { // 如果步可点击移除所有的事件检测
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { //如果已经出发长按事件,且mHasPerformedLongPress设置为true则不去执行单击
if (mPerformClick == null) {
mPerformClick = new PerformClick(); //创建PerformClick检测单击事件,最终调用 performClick();
}
if (!post(mPerformClick)) { //发送失败直接调用performClick()
performClick();
}
}
public boolean performClick() {
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this); // 调用onClick方法
result = true;
} else {
result = false;
}
}
在手指抬起时View执行以下操作:
public boolean performClick() {
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this); // 调用onClick方法
result = true;
} else {
result = false;
}
}
这个方法看起来是不是很面熟,和上面判断onTouch()的基本一致,首先判断View是否设置了OnClickListener事件监听,若设置则调用onClick()方法,此时result返回true表示消耗事件,所以我们设置的onClick的监听等级较低,按照事件分发逻辑看,处理我们触摸事件的方法按优先级以此为:onTouch() -> onTouchEvent() -> onClick();
View的事件传递到此就结束了,下面看看比他更复杂的、它的父类ViewGroup的事件分发;
前面分析了View的事件分发,但在实际开发过程中真正要使用View事件分发时,基本都是因为ViewGroup的嵌套导致的内外滑动问题,所以对ViewGroup的事件分发更需要深入了解,和View的事件分发一样,ViewGroup事件分发一样与几个方法有关:
使用一段伪代码来表述上面三个方法在ViewGroup事件分发中的作用,代码如下:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else{
consume = child.dispatchTouchEvent(event);
}
return consume;
}
从上面代码中看出,事件传递到ViewGroup时首先传递到dispatchTouchEvent(MotionEvent event)中,然后执行以下逻辑,首先在ViewGroup.dispatchTouchEvent() 中调用onInterceptTouchEvent() 方法:
在onInterceptTouchEvent() 返回false时,表明当前ViewGroup不消耗事件,此事件会向下传递给子View,此子View可能是View也可能是ViewGroup,如果是View则按照上面的事件分发消耗事件;
事件的传递首先是从手指触摸屏幕开始,所以我们先查看dispatchTouchEvent()中的ACTION_DOWN方法,剔除剩余复杂的逻辑,方法有一段主要的代码:
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 返回true表示子View设置了父容器不拦截事件
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
上述代码虽然简单但ViewGroup的事件分发多半与此处的逻辑有关,里面的每个细节都会影响到最终的事件消耗,总结上面代码执行如下:
在上述代码中除了MotionEvent.ACTION_DOWN和mFirstTouchTarget != null条件之外,还有一个会影响到onInterceptedTouchEvent()的调用,就是(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0,这里主要是用于在子View中设置父容器的拦截条件(多用于滑动冲突),先看以下FLAG_DISALLOW_INTERCEPT这个标识为:
看一下requestDisallowInterceptTouchEvent()方法源码:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // 状态相等时无需设定
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT; // mGroupFlags = FLAG_DISALLOW_INTERCEPT
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; // mGroupFlags = 0;
}
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
上面代码中mGroupFlags初始值为0,FLAG_DISALLOW_INTERCEPT初始值为0x80000,在方法中根据参数boolean设置mGroupFlags的值:
当传入disallowIntercept为true时,mGroupFlags = mGroupFlags | FLAG_DISALLOW_INTERCEPT = 0x80000;此时在dispatchTouchEvent()中 满足(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 即disallowIntercept = true,所以intercepted 直接返回false,不拦截事件
当传入disallowIntercept为false时,mGroupFlags = mGroupFlags & ~FLAG_DISALLOW_INTERCEPT = 0;此时在dispatchTouchEvent()中 不满足(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 即disallowIntercept = false,所以回调onInterceptTouchEvent(),父布局有机会拦截事件
总结一句话就是在requestDisallowInterceptTouchEvent()中设置true,表示不允许父容器拦截事件,设置为false,表示允许父容器拦截事件;
既然上面所有的条件都在判断是否需要调用onInterceptTouchEvent(),说明事件最后的拦截取决于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; //默认返回false,即父容器不拦截任何事件
}
if (!canceled && !intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) { //循环检测每个子View
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
…...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) { //检测当前坐标是否超出View的范围,若超出跳过此view
ev.setTargetAccessibilityFocus(false);
continue;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //调用dispatchTransformedTouchEvent方法
…...
newTouchTarget = addTouchTarget(child, idBitsToAssign); // addTouchTarget中赋值mFirstTouchTarget指向child
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
//dispatchTransformedTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event); // 如果child == null,直接调用super.dispatchTouchEvent,ViewGroup自己处理
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event); // 如果存在child,调用child.dispatchTouchEvent(event)
event.offsetLocation(-offsetX, -offsetY);
}
}
上面代码为ViewGroup的dispatchTouchEvent()中的部分代码,也是控制ViewGroup的事件传向子View的传递,一起来看一下执行逻辑:
在dispatchTransformedTouchEvent()中根据子View判断执行,如果child == null则直接调用super.dispatchTouchEvent,ViewGroup自己处理,如果存在child,调用child.dispatchTouchEvent(event),则事件传递到View,接着刚才的代码向下看,当dispatchTransformedTouchEvent()返回true时,代码会执行到addTouchTarget(child, idBitsToAssign)方法:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
在addTouchTarget()方法中将mFirstTouchTarget指向子View,所以上面的判断mFirstTouchTarget != null在子View拦截事件时成立;
到View的onTouchEvent()返回true即表示事件被View消耗,事件的分发也到此结束了,可有没有考虑过最上层的子View的onTouchEvent()如果不拦截事件呢?最终的事件会去哪呢?答案是要被Activity的onTouchEvent()消耗,我们知道当一个事件产生时最先获取的是Activity,然后按照Activity -》Window -》ViewGroup -》View这样的顺序传递下去,而在ViewGroup中子View的返回值是在dispatchTransformedTouchEvent()中获取的,查看代码:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
在dispatchTransformedTouchEvent()中若返回false,程序会执行到以下逻辑:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
通过上面的学习我们知道mFirstTouchTarget是指向消耗事件的子View,但当子View不消耗时此时mFirstTouchTarget == null成立,代码会再次调用dispatchTransformedTouchEvent()方法,此时传递的child为null,通过上面的代码我们知道child = null时代码执行super.dispatchTouchEvent(event),即调用父类的dispatchTouchEvent(event),因为ViewGroup本质上也是继承View,只不过是包含子View的View,所以事件的传递又到了上层View中,在View的dispatchTouchEvent()会询问onTouch()和onTouchEvent()方法,所以事件又被向上传递了;
但如果所有的ViewGroup和子View都不消耗事件,事件会逐层向上传递知道事件的开始,也就是Activity层,这时我们点开Activity的dispatchTouchEvent()方法,
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
从代码中可以看出系统调用getWindow().superDispatchTouchEvent(ev)进行事件分发,其实就是向Window和ViewGroup进行事件的传递,若有消耗事件的这里返回true方法结束,若没有View消耗事件即getWindow().superDispatchTouchEvent(ev)返回false,系统会调用Activity的onTouchEvent()处理事件,所以事件一定会被消耗掉,到此针对View不消耗事件的分析就结束了,我们也可以得出以下结论:
关于ViewGroup的事件分发的基本知识和源码分析到这里就介绍完了,可能直接理解会比较抽象,下面我们具体的看一下是如何控制和拦截事件的;
根据上面的View和ViewGroup的事件分发学习,这里给出几个View事件传递的结论(以下结论针对系统自动分发),并根据学习内容进行逐条分析
事件拦截最经典的使用示例和场景就是滑动冲突,按照View的冲突场景分,滑动冲突可以分为3类:
一般处理滑动冲突有两种拦截方法:外拦截和内拦截
外拦截顾名思义是在View的外部拦截事件,对View来说外部就是其父类容器,即在父容器中拦截事件,通过上面的代码我们知道,ViewGroup的事件拦截取决与onInterceptTouchEvent()的返回值,所以我们在ViewGroup中重写onInterceptTouchEvent()方法,在父类需要的时候返回true拦截事件,具体需要的场景要按照自己的业务逻辑判断:
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercept = false
when(ev!!.action){
MotionEvent.ACTION_DOWN ->{intercept = false}
MotionEvent.ACTION_MOVE->{
intercept = if (isNeed()){
true
}else{
false
}
}
MotionEvent.ACTION_UP->{intercept = false}
}
return intercept
}
从上面代码中看出:在onInterceptTouchEvent()的ACTION_DOWN中必须返回false,即不拦截ACTION_DOWN事件,因为如果ACTION_DOWN一但拦截,事件后面的事件都会默认给ViewGroup处理,也不会再调用onInterceptTouchEvent()询问拦截,那子View将没有获取事件的机会;在ACTION_DOWN中,根据自己需要的时候返回true,那此时事件就会被父ViewGroup消耗
内拦截是在View的内部控制父容器是否拦截事件,你可能已经想到了就是使用上面介绍的requestDisallowInterceptTouchEvent(),答案没错就是利用这个方法,关于使用这个方法去控制mGroupFlags的值上面已经介绍了,下面我们分析下为何设置此数据来控制ViewGroup的事件拦截:
因为事件的拦截是在onInterceptTouchEvent()中确定的,我们不可能在子View中控制父容器的方法,但从上面的代码中看出,ViewGroup访问onInterceptTouchEvent()之前必须通过一段关卡,就是(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 要成立,而如果此条件不成立,那dispatchTouchEvent()会直接返回false,所以我们在子View中只要控制这个值就可以了;
到此虽然可以控制访问权限,但如何确保只要在允许访问的时候就会自动拦截呢?那就是onInterceptTouchEvent()要在特定状态下一直返回true,即默认想拦截事件 ,综上所述我们在子View中要想控制父容器必须满足以下条件:
上面的事件分发,其实和公司安排任务一样,当一项任务来临时,公司会开会进行任务安排,你可能做好了承担一切任务的准备,但大领导不询问你,整个事件就会按照领导的意见进行安排,突然在某个任务时大领导问了你愿不愿意接,这时你提出了肯定的答复,然后事情就归你了 ,当然干好干不好就是你的问题了,拦截的情况和这个例子一样,下面看下拦截的代码:
//在子View中重写dispatchTouchEvent()方法控制父类的拦截
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
y = event.getY();
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float currentY = event.getY();
int minTouchSlop = 150;
if (Math.abs(currentY - y) >= minTouchSlop) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
//在ViewGroup中拦截除ACTION_DOWN以外的事件
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercept = false
when(ev!!.action){
MotionEvent.ACTION_DOWN ->{intercept = false}
else -> {intercept = true}
}
return intercept
}
到此View和ViewGroup的事件分发和事件滑动冲突的处理到此介绍完毕了,虽然很早之前就学习过这部分的内容,但并没有很好的整理这部分内容,自己写一遍会对整个只是点更加详细的理解,相信在开发过程中很多人都被滑动冲突困扰过,尤其对初级开发者,那段痛苦是必须经过的,所以只有熟悉View和ViewGroup的事件分发逻辑,才能从根本上解决实际开发中的问题