前言
1、Android 输入事件一撸到底之源头活水(1)
2、Android 输入事件一撸到底之DecorView拦路虎(2)
3、Android 输入事件一撸到底之View接盘侠(3
前两篇文章分别分析了输入事件分发到App层以及DecorView对输入事件的处理,最终交给ViewTree处理。我们平时对事件的处理大部分集中在对ViewTree的处理上,网上绝大部分的文章也是针对此分析,为了将输入事件连贯起来,从总体看局部,由局部推总体,接下来分析ViewTree的事件分发。
通过本篇文章,你将了解到:
1、View/ViewGroup/ViewTree 易混淆之处
2、ViewGroup 事件分发
3、View 事件分发
4、ViewTree 事件分发
5、事件分发系列总结
一个小比喻
对代码调用流程比较疑惑的话,我们做个简单的比喻:
一个学校有3个年级,每个年级有1个年级主任、3个班,每个班有个班主任、有若干个学生,其中一个学生叫小明
有一天,校长接到了个任务:有个日本的女学生:石原里美要来学校做交流。
校长将这个任务指派下去,先找到1年级主任问你们年级要接收这个学生不?年级主任想到手底下还有几个班,就先问1班班主任你们能接收吗,1班主任想这么多学生我问问谁能带带这个石原里美,就先问小明。这个过程叫做dispatchTouchEvent。
小明接到任务,发现手底下没人了(自己是View,班主任、年级主任、校长是ViewGroup),就只能自己硬着头皮看看这石原里美资料,这时候他有两个选择:接受/拒绝
小明看了资料发现石原里美是个小美女,于是开心选择了接受,那么就答应班主任,班主任将结果告诉年级主任,年级主任告诉校长,校长长叹一口气,终于将包袱甩掉了。。这个过程就是dispatchTouchEvent 回传结果。
某天石原里美的同班男同学:松井 听说石原里美在中国玩得挺开心的,自己也想来。校长架不住,只能答应。校长知道石原里美在1年级里做交流,想想松井和石原里美是同学,在一起更好交流,于是直接就将这个任务交给了1年级主任,1年级主任知道石原里美在1班,于是直接交给了1班,1班主任知道小明接待过石原里美,就让小明陪松井(石原里美是Down事件,松井是后续的Move、Up事件),小明心里一万只草泥马,谁叫自己冲动了呢(自己xxx,跪着也要完成)。这个过程就是某布局一旦处理了Down事件,那么后续的事件都会通知给它。
时光回到过去,小明是个刚正不阿的学生,表示自己学习很忙,书中自有黄金屋,书中自有颜如玉,没时间陪伴石原里美,这时他告诉班主任他不接这个任务,班主任一看,班里没人愿意接这活儿了,幸好还有B计划,就先看看自己能完成这个任务不。如果不能完成,还是交给领导来做吧,交给了年级主任,年级主任暗自庆幸自己也有B计划,发现自己B计划能完成,于是就亲自带石原里美了。校长知道年级主任接收任务了,很开心,自己的B计划终于没有摆上台面。这个B计划就是onTouchEvent
当然,松井同学最后也交给了年级主任带。。
中间有个插曲,班主任看了石原里美资料,心里有个大胆的想法:自己的儿子和她差不多年级,多交流一下提高儿子的外语水平,多好啊。于是当他从年级主任那收到这个任务的时候,就不把这个任务发下去了,告诉年级主任自己可以处理。这个过程就是 onInterceptTouchEvent
当然小明也不是没机会陪伴石原里美,学校之前制定了规矩:任何人都可以禁止上级不将任务派给自己(领导不能私自将任务拦截了,至少得通知下级),小明使用了这条规定,班主任的大胆想法碎了一地。。当然这条规定一视同仁,包括年级主任、校长都要遵守。
这个过程就是 requestDisallowInterceptTouchEvent
View/ViewGroup/ViewTree 易混淆之处
父类/子类、父布局/子布局
网上一些文章在画关系图的时候没有将两者区分开,容易让刚接触此内容的人混淆。先看看View、ViewGroup定义:
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {}
public abstract class ViewGroup extends View implements ViewParent, ViewManager {}
可以看出ViewGroup 继承自View,也即是ViewGroup是View的子类,View是ViewGroup的父类。
父类/子类关系是语言范畴的关系:
子类访问父类的方法:
super.doMethod(xx)
再来看看常见的布局文件内容:
FrameLayout是ViewGroup子类,该布局文件里,FrameLayout是父布局,View是子布局。从ViewGroup的命名可以看出,ViewGroup是View的集合,当然ViewGroup也可以是ViewGroup的集合(嵌套)。
父布局/子布局关系是ViewGroup/View 里定义的。
子布局寻找父布局
public final ViewParent getParent() {
return mParent;
}
- ViewParent 是个接口,ViewGroup、ViewRootImpl 都实现了它
获取到mParent后需要强转为对应类型- 每当将子布局添加到父布局里的时候,就给子布局指定其父布局,也就是给mParent赋值 (assignParent(xx))
- RootView(如DecorView)的mParent指向ViewRootImpl (setView(xx) 里指定)
父布局寻找子布局
父布局通过addView(xx)方法将子布局添加到一维数组里,因此父布局寻找其子布局也即是访问该组数的过程:
public View getChildAt(int index) {
if (index < 0 || index >= mChildrenCount) {
return null;
}
//private View[] mChildren;
return mChildren[index];
}
ViewTree 建立
了解父布局、子布局关系,将子布局添加到父布局里,这是最简单的ViewTree结构。再将父布局作为另一个父布局的子布局添加,那么ViewTree又增加了一层。如此反复,最终形成的ViewTree 如下:
注意:ViewTree并不是一个类,仅仅只是为了方便描述View/ViewGroup构成的布局层次而命名的。
由此可知:
- View是ViewGroup父类
- View只能作为子布局,不能作为父布局
- ViewGroup既可做父布局,也可做子布局
厘清了上述概念,接下来进入正题。
ViewGroup 事件分发
回顾上篇文章内容:
1、DecorView 将事件传递给Activity处理,如果Activity没有处理,那么传递到Window进而传递到DecorView
2、DecorView 调用父类的dispatchTouchEvent(xx)方法继续分发事件
可以看出,此处重点是父类的dispatchTouchEvent(xx)方法。
DecorView继承自FrameLayout,在FrameLayout里寻找该方法,发现FrameLayout并没有重写该方法。而FrameLayout继承自ViewGroup,在ViewGroup里找到了dispatchTouchEvent(xx)。因此DecorView最终调用的是ViewGroup的dispatchTouchEvent(xx)方法。
一个小例子
如上图所示,ViewGroup里4个子布局,添加顺序如下:
addView(View1)
addView(View2)
addView(View3)
addView(View4)
View1、View2、View3相交于 "1"的位置
View3、View4相交于"2"的位置
当分别点击"1"、"2" 位置时,事件时怎么传递的呢?
先说结论:
当点击"1" 位置时:
1、ViewGroup 首先收到事件,并查找1位置是否落在某个子布局之内
2、ViewGroup 有4个子布局,倒序遍历寻找,也就是View4->View1的顺序寻找
3、先判断View4,"1"不在View4内,继续寻找
3、然后找到View3,判断View3是否想处理该事件,如果处理,那么事件不再传递给View1、View2;如果不接收,继续判断View2;
4、View2不处理,继续判断View1。
5、当View3、View2、View1 都不处理事件,那么只能交给他们的父布局ViewGroup。
6、当ViewGroup自身也不想处理,那么退回给它的父布局,其父布局的操作和ViewGroup对事件的分发一样的原理。
当点击"2" 位置时,与点击"1" 位置类似,不再赘述。
实际上上述事件分发的流程就是由ViewGroup dispatchTouchEvent(xx)方法完成,来看看它的源码:
ViewGroup dispatchTouchEvent(MotionEvent ev)
ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//标记该事件是否已处理
boolean handled = false;
//如果该View没有被遮挡,那么可以接收事件
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
//如果是Down事件,表明是一次事件序列的开始
//清空之前的状态
//清空touchTarget链表
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//标记是否拦截该事件
final boolean intercepted;
//两个条件满足一个即可
//1、是Down事件 2、mFirstTouchTarget != null 表示有子布局处理了Down事件,也就是子布局在收到Down事件时返回了true
//mFirstTouchTarget -> 指向链表头
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//查看标记位:自己能否被允许拦截事件 disallowIntercept=true 表示不被允许拦截事件------(1)
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//能够拦截事件,调用ViewGroup onInterceptTouchEvent 进行拦截------(2)
//返回值表示是否已处理该事件
intercepted = onInterceptTouchEvent(ev);
//恢复事件防止之前中途修改过
ev.setAction(action);
} else {
//不允许拦截,则肯定未处理
intercepted = false;
}
} else {
//如果不是Down事件且也没有任何子布局处理过Down事件,则表示ViewGroup已经拦截处理该事件
intercepted = true;
}
...
//记录处理了Down事件的链表
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//事件未取消且没被拦截处理
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex();
//单指、多指Down事件,鼠标事件
//ViewGroup 直接子布局个数
final int childrenCount = mChildrenCount;
//有子布局且还未有任何子布局处理过事件
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
//根据绘制顺序生成接收事件的子布局列表,一般都忽略
final ArrayList preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//倒序寻找子布局,addView(xx1) addView(xx2)是正序
for (int i = childrenCount - 1; i >= 0; i--) {
//先确定待检测的子布局
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
//child.canReceivePointerEvents() -> 能否接收事件,也就是子布局是否可见 Visible 不可见无法接收事件
//isTransformedTouchPointInView() -> 检测当前点击的点是否落在子布局内,检测时候考虑了子布局的padding/scroll/matrix 对位置的影响
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
//不满足条件,则跳过该子布局,继续寻找另一个布局
continue;
}
//上述条件满足了,检测该子布局是否已经处理过该Down事件
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
//处理过直接跳出循环,不用再找下一个子布局了
break;
}
//该子布局还未处理过Down事件
//将事件分发给子布局->child ------(3)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//返回true->子布局已经处理了该Down事件
mLastTouchDownTime = ev.getDownTime();
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//将子布局构成的TouchTarget挂到链表头(链表节点表示处理了Down事件的子布局)
//mFirstTouchTarget 指向当前链表头(也即是mFirstTouchTarget有值了,重要!)
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//标记Down事件已经找到处理者了
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
if (preorderedList != null) preorderedList.clear();
}
...
}
}
//没找到任何处理了Down事件的子布局
if (mFirstTouchTarget == null) {
//因为没有找到任何处理了Down事件的子布局,因此事件分发是目标布局填:null ------(4)
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) {
//Down事件,之前已经处理过了,这里直接标记位已吹李
handled = true;
} else {
//Down之外的其它事件,如Move Up等
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//target.child 为目标子布局,分发给它处理 ------(5)
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
//如返回true,标记为已处理
handled = true;
}
if (cancelChild) {
//如已取消,则跳过当前继续寻找下一个节点
}
}
//继续寻找下一个需要处理的节点 一般来说该链表通常只有一个节点
predecessor = target;
target = next;
}
}
...
}
...
//最终返回dispatchTouchEvent(xx)该方法对事件处理结果
//true->已处理 false->未处理 ------(6)
return handled;
}
注意,为了理解方便,上面以子布局代替子View阐述,子布局可以是View也可以是ViewGroup。
上边注释比较比较清晰了,列出了(1)~(6)比较重要的点:
(1)
禁止拦截标记:
ViewGroup.java
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
//已设置过了,无需再次设置
return;
}
//设置标记位
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
if (mParent != null) {
//递归调用父类,直至ViewRootImpl
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
该方法为ViewGroup独有,若某个子布局不想让其父布局拦截其事件序列,那么调用getParent().requestDisallowInterceptTouchEvent(true)即可。该方法一直往上追溯设置父布局,也就是子布局之上的所有层次的父布局不拦截事件
(2)
只有ViewGroup 有onInterceptTouchEvent(xx)方法,View没有。该方法是为了在事件分发给子布局之前进行拦截操作。
ViewGroup.java
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;
}
如果不重写onInterceptTouchEvent(xx)方法,默认不拦截事件。当重写该方法进行拦截的时候需要注意:
onInterceptTouchEvent(xx)拦截到Down事件后,如果此时没有任何子布局处理Down事件,那么后续的Move、Up等事件onInterceptTouchEvent(xx) 将不会收到,也就是onInterceptTouchEvent不执行
一般很少重写ViewGroup dispatchTouchEvent(xx),处理事件使用onInterceptTouchEvent(xx) + onTouchEvent(xx) 处理
(3)
将事件分发给子布局
ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
//child 指的是要接收事件的子布局
final boolean handled;
...
if (child == null) {
//如果没有子布局接收Down事件,那么Down/Move/Up事件直接调用父类处理方法
//ViewGroup 父类就是View 因此调用的是View的dispatchTouchEvent(xx)方法
handled = super.dispatchTouchEvent(transformedEvent);
} else {
//有子布局接收Down事件
//计算子布局位置偏移
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
//将Event 位置偏移,使它落在子布局内
transformedEvent.offsetLocation(offsetX, offsetY);
//考虑matrix 对位置的影响
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
//Event位置调整后,它的位置是基于当前子布局的左上角为原点偏移的
//继续分发给子布局 child可能为View也可能为ViewGroup,如果是ViewGroup那么又重新走到了其父布局的分发逻辑
//继续递归分发,直至返回
handled = child.dispatchTouchEvent(transformedEvent);
}
//处理结果
return handled;
}
1、该方法递归派发事件
2、MotionEvent修改的是AXIS_X、AXIS_Y值,也就是Event.getX()、Event.getY()取得的值,这也就是为什么这两个值是距离当前View左上角的原因
(4)
此处方法
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
传入的是null,因此交给View dispatchTouchEvent(xx)处理
(5)
当有子布局处理了Down事件,那么后续的Move、Up等事件传递过来后,直接派发给当初处理了Down事件的子布局。
由此可以看出:
1、Down事件是事件序列(Down->Move->Up)的开始,如果某个布局没有处理Down事件,那么后续的事件将收不到
2、如果某个布局处理了Down事件,那么即使父布局拦截(onInterceptTouchEvent(xx))了事件,子布局依然能够收到完整的事件序列
(6)
最终事件的处理结果体现在一个布尔值上。
true -> 表示该事件已处理
false -> 表示该事件未处理
需要注意的是:
不管是否对事件"真正处理",只要返回true,就告诉调用者该事件已处理。即使对事件做了"很多处理",返回false,就告诉调用者该事件未处理。
上面分析了一堆可能比较混淆,用图表示ViewGroup 分发事件的过程:
ViewGroup 事件分发常用方法
以上,分析了ViewGroup分发事件的逻辑,接下来看看事件分发到View时如何处理。
View 事件分发
从ViewGroup分发逻辑可以看出:ViewGroup分发事件的过程就是递归查找子布局并分发。那么递归结束的条件是什么呢?
1、有子布局处理了该事件,最终调用View.dispatchTouchEvent(xx)
2、没有子布局处理该事件,只能自己接收(不一定处理),此时调用super.dispatchTouchEvent(xx),也就是View.dispatchTouchEvent(xx)
由此可知,ViewGroup分发事件最终需要交给View.dispatchTouchEvent(xx)处理。
View dispatchTouchEvent(MotionEvent ev)
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (actionMasked == MotionEvent.ACTION_DOWN) {
//停止滑动
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//如果注册了onTouch回调,则执行onTouch方法
//如果该方法返回true,则认为该事件已经处理了
ListenerInfo li = mListenerInfo;
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)) {
stopNestedScroll();
}
return result;
}
onTouch通过以下方法注册:
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
如果onTouch(xx)处理了事件,那么onTouchEvent(xx)就不会调用
onTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event) {
//获取点击坐标
final float x = event.getX();
final float y = event.getY();
//View控制标记,根据标记内容控制View的属性
final int viewFlags = mViewFlags;
//点击动作->Down/Move/Up等
final int action = event.getAction();
//如果该View设置了:可以单击、可以长按、鼠标右键弹出(很少用)中的一个
//那么认为该View可以点击-------(1)
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
//View默认是ENABLED,若是DISABLED,则返回clickable
return clickable;
}
if (mTouchDelegate != null) {
//用在子布局扩大其点击区域使用,当点击坐标位于子布局之外时,通过该方法判断点击坐标是否位于子布局的"扩大区域内"
//若是则将事件交给子布局处理---------(2)
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//View可点击或者鼠标移动悬浮显示(很少用)
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
//在Down事件里已经处在按下状态
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
//如果处在焦点获取状态但又未获得焦点,则主动申请焦点
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
//mHasPerformedLongPress 表示长按事件是否已经处理了事件
//如果已经处理了,则单击事件不会执行
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
//长按事件还没来得及处理,此处将长按事件移除
removeLongPressCallback();
//如果上一步是请求获取焦点并成功了,则这一次不处理后续事件
if (!focusTaken) {
if (mPerformClick == null) {
//实现Runnable接口的类,用于回调
mPerformClick = new PerformClick();
}
//为了给View更新其他状态留够时间,此处是通过Handler发送到主线程执行------(3)
if (!post(mPerformClick)) {
//如果不成功,则直接调用
performClickInternal();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
...
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
...
//在可滚动的容器内,为了容错,延迟点击
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
//设置按下的状态,多用于按下时背景/前景等变化
setPressed(true, x, y);
//开启一个长按延时事件,当延时事件到了就执行该事件(长按事件)
//ViewConfiguration.getLongPressTimeout() 就是长按的时间阈值。不同系统可能不一样
//我手机上是400ms,缺省值是500ms----------(4)
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
//只要是clickable=true 则认为已经处理了该事件
return true;
}
return false;
}
和ViewGroup分析类似,上边注释比较比较清晰了,列出了(1)~(4)比较重要的点:
(1)
CLICKABLE/LONG_CLICKABLE 相关
赋值操作:
View.java
public void setClickable(boolean clickable) {
setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
}
public void setLongClickable(boolean longClickable) {
setFlags(longClickable ? LONG_CLICKABLE : 0, LONG_CLICKABLE);
}
当前也可以在XML里指定属性。你可能比较疑惑,一般我们都不用设置上面的属性,View.onTouchEvent(xx)依然能够执行,咋回事呢?这得要分两种情况:
1、View CLICKABLE/LONG_CLICKABLE 属性默认是没有设置的,比如TextView就没设置CLICKABLE,但是Button在其默认属性了设置了CLICKABLE
2、不管有没有设置上述属性,只要调用了View.setOnClickListener(xx)/View.setOnLongClickListener,这俩方法内部分别调用了View.setClickable(xx)/View.setLongClickable(xx)方法
(2)
通常来说,内容区域不变,扩大View的点击区域有两种方法:
1、设置padding
2、设置TouchDelegate
简单说说TouchDelegate,顾名思义,Touch事件代理。使用方法如下:
//view为待扩大的点击区域
view.post(new Runnable() {
@Override
public void run() {
Rect areaRect = new Rect();
//获取原本的区域
view.getHitRect(areaRect);
//将区域扩大
areaRect.left -= 100;
areaRect.top -= 100;
areaRect.right += 100;
areaRect.bottom += 100;
View parentView = (View)view.getParent();
//设置代理,当点击坐标落在areaRect之内时,事件优先交给view处理
TouchDelegate touchDelegate = new TouchDelegate(areaRect, view);
//给父布局设置代理,事件流转:子布局的onTouchEvent->父布局的onTouchEvent->落在扩大的区域内->将坐标值更改->子布局的dispatchTouchEvent->子布局onTouchEvent
parentView.setTouchDelegate(touchDelegate);
}
});
(3)
PerformClick
最终调用到
public boolean performClick() {
...
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
//声音反馈
playSoundEffect(SoundEffectConstants.CLICK);
//熟知的onClick
li.mOnClickListener.onClick(this);
//只要执行了onClick,就认为已经处理了事件
result = true;
} else {
result = false;
}
...
return result;
}
我们平时给View设置的点击事件:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
就是此时回调的。
(4)
来看看长按事件
最终调用CheckForLongPress里的Run方法
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
recordGestureClassification(mClassification);
if (performLongClick(mX, mY)) {
//记录长按事件已经处理了
mHasPerformedLongPress = true;
}
}
}
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) {
//执行 setOnLongClickListener(xx) 注册的回调
handled = li.mOnLongClickListener.onLongClick(View.this);
}
...
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
长按和短按的区别
- 重写onLongClick(View v),其返回值决定是否执行onClick(View v)方法,若是返回true表示已经处理长按事件,短按无需处理了
- 重写onClick(View v),该方法没有返回值,执行了该方法就不会执行onLongClick(View v)
用图表示View事件分发流程:
View 事件分发常用方法
值得注意的是onTouch(xx)回调和短按操作区别:
短按是在收到Up事件后触发的,而onTouch(xx)则是只要收到事件就会触发
ViewTree 事件分发
以上分别分析了ViewGroup、View的事件分发流程,而众多ViewGroup、View组成了ViewTree结构。将父、子布局的dispatchTouchEvent、onTouchEvent关联起来,如图:
从中可以看出:
- 若是父布局dispatchTouchEvent处理了事件,那么子布局的dispatchTouchEvent将收不到事件
- 若是子布局的onTouchEvent处理了事件,那么父布局的onTouchEvent将收不到事件
ViewGroup/View 事件分发难点在:
弄清楚子布局/父布局、View/ViewGroup继承关系
事件分发系列总结
Android事件分发系列文章分了三篇来讲述
Android 输入事件一撸到底之源头活水(1)
分析了App层从底层收到事件后ViewRootImpl.java的处理
Android 输入事件一撸到底之DecorView拦路虎(2)
分析了DecorView对事件的处理
Android 输入事件一撸到底之View接盘侠(3)
前面事件没有处理,流转到此处进行处理后就完成了使命,这也就是为什么本篇文章叫做"View接盘侠的原因"
网上很多文章将DecorView与View/ViewGroup事件处理一起讲解,没有明确指出两者之间的差异。通过本系列文章,我们知道DecorView对事件的处理并不是必须的,只有使用了DecorView作为RootView才特殊处理(比如Activity、Dialog等)。
将三者串联起来:
一般来说,我们常接触到的就是第二部分、第三部分,尤其是第三部分常用。
建议将这三部分对应的文章结合起来看,局部---->整体---->局部,这样对整个事件分发有个更清晰的认识。
本文基于 Android 10.0 源码