Android面试Android进阶(十六)-事件分发相关
问:描述一下Android事件分发流程
答:Android事件指的是:MotionEvent的四种状态(ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL),Android的事件是从Activity开始的,中间经过Window及DecorView,其中Window的唯一实现类为PhoneWindow,DecorView是一个FrameLayout(其中有一个LinearLayout,id为content)的顶级View。之后通过dispatchTouchEvent方法分发到ViewGroup、View。即整个事件分发的流程是:
Activity -》PhoneWindow -》DecorView -》ViewGroup -》View
各个层级都可以对其进行消费,一旦消费则不再往下分发。
如果事件分发过程中都没有进行处理,则会进行反方向回传,最终回传给Activity,如果Activity也没有处理,则抛弃本次事件
View -》ViewGroup -》DevorView -》PhoneWindow -》Activity
这里其实是一个设计模式的使用:责任链模式,有想了解的可以去了解一下。
PhoneWindow、DecorView这两个步骤在我们平时及自定义View时不需要进行处理,这里简单说一下Activity、ViewGroup、View的事件分发。
类型 | 方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | true | true | true |
事件拦截 | onInterceptTouchEvent | false | true | false |
事件消费 | onTouchEvent | true | true | true |
从上表可以看出,Activity和View是没有事件拦截的,因为:
1、Activity作为事件开始的地方,原始事件分发者如果拦截了,将导致整个屏幕都无法响应事件。
2、View作为事件传递的最终端,要么消费事件、要么回传事件,没有必要进行拦截。
举几个形象生动的例子来说明dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent的关系,首先先把角色定义一下。就拿公司组织架构来讲:
Activity:老板
//MainActivity,重写dispatchTouchEvent、onTouchEvent方法
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.e("XYX", "MainActivity-dispatchTouchEvent 老板:经理,我看这个电商行业现在很火啊,看着模仿一个淘宝出来吧,下周能做好吗?")
return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("XYX", "MainActivity-onTouchEvent 老板:都干啥吃的,这么简单的东西都做不了")
return super.onTouchEvent(event)
}
}
ViewGroupA:项目经理
//ViewGroupA:继承自LinearLayout,是一个ViewGroup,重写分发、拦截、消费事件方法:
class ViewGroupA : LinearLayout {
/**
* 在xml布局文件中使用时自动调用
*/
constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupA-dispatchTouchEvent 项目经理:呼叫技术部,老板要做一个淘宝,你们评估一下,下周能不能上线")
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupA-onInterceptTouchEvent 项目经理:老板可能疯了,但是又不是我做,问问下面的人")
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupA-onTouchEvent 项目经理:报告老板,技术部说做不了(得把锅甩出去)")
return super.onTouchEvent(event)
}
}
ViewGroupB:技术总监
//ViewGroupB:技术总监 继承自LinearLayout,是一个ViewGroup,重写分发、拦截、消费事件方法:
class ViewGroupB : LinearLayout {
/**
* 在xml布局文件中使用时自动调用
*/
constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupB-dispatchTouchEvent 技术总监:做一个淘宝?下周要上线?疯了吧!")
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupB-onInterceptTouchEvent 技术总监:肯定做不了,给小王传达一下老板的意思吧")
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupB-onTouchEvent 技术总监:小王说做不了(锅得甩出去,不是我做不了)")
return super.onTouchEvent(event)
}
}
View1:小王(就是你这个Android开发了)
//ViewA 小王 继承自View
class ViewA : View {
/**
* 在xml布局文件中使用时自动调用
*/
constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.e("XYX", "ViewA-dispatchTouchEvent 小王:心里一万只草泥马飘过,老板疯了")
return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("XYX", "ViewA-onTouchEvent 小王:虽然一万只草泥马,但是也要给回应啊,毕竟都是打工人:老大,这个我真心做不了啊")
return super.onTouchEvent(event)
}
}
显示效果:
image.png
测试一下没有任何拦截与消费的情况下,全部默认实现时,点击ViewA后打印结果如下:
image.png
上面看见的老板提出这个需求以后,所有人都不发表意见,就把问题需求分发下去了,任何人都处理不了,交回给老板处理去了,老板只能大骂一声:都干啥吃的,这么简单都做不了!现在改一下,老板感觉小王能力太差,把他开了,招了一个Android大牛小李回来,新来的想要表现一下,什么活都能干,这不他就接下来一周做一个淘宝了:
//修改ViewA中onTouchEvent方法:
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("XYX", "ViewA-onTouchEvent 小李:好的,交给我吧,一周做不完提头来见!")
// return super.onTouchEvent(event)
return true
}
打印结果:
image.png
一周过去了,小李没有提头来见,而是被老板开了(什么原因大家心里清楚),又新找了一个新的小赵来。这个时候,技术总监一看新来的小伙子挺有出息,而且一周做一个淘宝,技术总监心里难道没点逼数么,所以任务到技术总监那的时候就不往下传达了,免得小赵压力太大就跑路了,招个好的技术本来就是那么困难,跑了就没人给他干活了对吧
修改ViewGroupB代码:
//修改拦截返回true,不再分发事件
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupB-onInterceptTouchEvent 技术总监:肯定做不了,不做小赵压力了")
return true
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupB-onTouchEvent 技术总监:这个需求一周做不了,给我一个阿里都搞不定,更别说一个小赵了")
return super.onTouchEvent(event)
}
打印结果:
image.png
老板一气之下又把技术总监给开了,老板还为了节省开支,换了一个毫无情商的技术总监,收到任务,也不问问下面的人能不能做,自己认为不能做,然后也不给上面答复能不能做。
修改代码:
//自己就消费了事件,不给上级回传了,返回 true
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupB-onTouchEvent 技术总监:这个需求一周做不了,给我一个阿里都搞不定,更别说一个小赵了")
return true
}
打印结果:
image.png
技术总监没有给项目经理回复,老板等了好多天没有得到消息,自然气不过,老板二话不说把项目经理给开了,虽然是技术总监没给回复,但是你自己不会催嘛?然后老板又招了一个新的项目经理,但是这个项目经理更是扯淡,成天吊儿郎当的,收到任务不给分发也不处理,收到就收到了...
修改ViewGroupA代码:
//分发中直接返回true
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.e("XYX", "ViewGroupA-dispatchTouchEvent 项目经理:我知道了...")
return true //或者return false
}
打印结果:
image.png
这么下去,老板就要开始自闭了...
总结一下:
1、View和ViewGroup的区别在于ViewGroup中多一个onInterceptTouchEvent()拦截方法
2、三个方法默认都返回false,分发流程:dispatchTouchEvent -》onInterceptTouchEvent -》下级 dispatchTouchEvent -》onInterceptTouchEvent -》 ...直到 View的dispatchTouchEvent -》onTouchEvent -》上级onTouchEvent...直到Activity的onTouchEvent -》事件分发结束
3、在onTouchEvent方法中,如果返回true,则消费事件,不再回传给上级
4、在onInterceptTouchEvent方法中,如果返回true,则拦截事件,回调自身onTouchEvent方法,再根据onTouchEvent方法返回值判断是否回调上级的onTouchEvent
5、在dispatchTouchEvent方法中,如果直接返回true / false,不使用super. dispatchTouchEvent(ev)的话,则分发结束,不再回调自身onTouchEvent方法,。
注意:dispatchTouchEvent 方法返回的true和false的区别是true会执行action_down、action_move和action_up,而如果直接返回false只会执行到action_down。有兴趣的话,可以看一下dispatchTouchEvent方法的源码,这个方法中会在action_down时调用 dispatchTransformedTouchEvent方法,这里不详细说明了。
问:View什么会有dispatchTouchEvent方法?View事件相关的各个方法的调用顺序是怎样的?
答:ViewGroup有ChildView,所以一定需要分发,View还可以注册很多事件监听器:onClick、onLongClick、onTouch等,这些监听器的的管理也需要用dispatchTouchEvent管理。所以View相关的各个方法也就是这几个,它们的调用顺序:
1、onClickListener单击事件需要有两个事件(ACTION_DOWN、ACTION_UP)即按下和抬起操作才会产生onClickListener事件
2、onLongClickListener长按事件只需要ACTION_DOWN事件,但是需要长时间按住才会产生,所以onLongClickListener事件会比onClickListener事件之前。
3、onTouchListener触摸事件,只需要触摸即可产生,其实也是ACTION_DOWN事件,如果注册了触摸事件,消费了就不会有onLongClickListener,所以onTouchListener应该排在onLongClickListener之前。
4、onTouchEvent View自身的处理事件,也是一种触摸事件,但是我们自己注册的触摸事件会排在它的前面。
总结一下就是:自己注册的触摸事件(onTouchListener)-》自身的处理事件(onTouchEvent)-》长按事件(onLongClickListener)-》单击事件(onClickListener)
代码示例一下,还是刚刚那个,新建一个ViewB,给B设置各类事件:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//设置长按事件
view_b.setOnLongClickListener {
Log.e("XYX", "OnLongClickListener")
false
}
//设置点击事件
view_b.setOnClickListener {
Log.e("XYX", "OnClickListener")
}
//设置触摸事件
view_b.setOnTouchListener { _, event ->
var str = ""
when(event.action){
MotionEvent.ACTION_DOWN->{
str = "ACTION_DOWN"
}
MotionEvent.ACTION_UP->{
str = "ACTION_UP"
}
}
Log.e("XYX", "OnTouchListener--$str")
false
}
}
}
class ViewB : View {
/**
* 在xml布局文件中使用时自动调用
*/
constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
var str = ""
when(event?.action){
MotionEvent.ACTION_DOWN->{
str = "ACTION_DOWN"
}
MotionEvent.ACTION_UP->{
str = "ACTION_UP"
}
}
Log.e("XYX", "onTouchEvent--$str")
return super.onTouchEvent(event)
}
}
打印结果:
image.png
View所有的事件分发都在dispatchTouchEvent方法中,看一下源码:
public boolean dispatchTouchEvent(MotionEvent event) {
//省略大量代码...
boolean result = false; // result 为返回值,主要作用是告诉调用者事件是否已经被消费。
if (onFilterTouchEventForSecurity(event)) {
/**
* 如果设置了OnTouchListener,并且当前 View 可点击,就调用监听器的 onTouch 方法,
* 如果 onTouch 方法返回值为 true,就设置 result 为 true。
*/
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
/**
* 如果 result 为 false,则调用自身的 onTouchEvent。
* 如果 onTouchEvent 返回值为 true,则设置 result 为 true。
*/
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
//看看onTouchEvent方法:
public boolean onTouchEvent(MotionEvent event) {
//省略大量代码...
final int action = event.getAction();
// 检查各种 clickable
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
//...
removeLongPressCallback(); // 移除长按
//...
performClick(); // 检查单击
//...
break;
case MotionEvent.ACTION_DOWN:
//...
checkForLongClick(0); // 检测长按
//...
break;
//...
}
return true; // ◀︎表示事件被消费
}
return false;
}
这里:
1、如果设置了OnTouchListener,并且View可点击,就会调用OnTouchListener.onTouch方法,判断onTouch返回值,作为dispatchTouchEvent方法的最后的返回值。
2、如果上面的返回值为false,就会执行View自身的onTouchEvent方法,并且如果onTouchEvent方法返回true,则将dispatchTouchEvent方法设置最后的返回值为true
3、onTouchEvent方法中,检查View是否可以点击(clickable、long_clickable、context_clickable)等,在DOWN事件下检测是否是长按、UP事件下移除长按,同时检测是否单击。
4、在onTouchEvent代码中,在第一个判断clickable时,代码块中直接就return true,表明只要View设置了clickable为true,则View自身就会消费事件 这也是我们在开发中常见的在View或者ViewGroup的上层再加一个View时,只要设置了点击事件或clickable时,点击上层下层是不会响应的机制。如:
<RelativeLayout
android:background="#CCC"
android:id="@+id/layout"
android:onClick="myClick"
android:layout_width="200dp"
android:layout_height="200dp">
<View
android:clickable="true"
android:layout_width="200dp"
android:layout_height="200dp" />
</RelativeLayout>
myClick的点击方法永远不会执行,因为View设置了clickable为true,不会回调Relativelayout的onTouchEvent方法
重点重点重点,重要要说三遍:所以View事件相关的各个方法调用顺序为:自己注册的触摸事件(onTouchListener)-》自身的处理事件(onTouchEvent)-》长按事件(onLongClickListener)-》单击事件(onClickListener)
上面的面试问题其实也已经分析完毕了,既然这里对View事件分发的源码分析了,接下来就顺带看看ViewGroup的事件分发源码吧。其实ViewGroup和View非常的类似,只不过是增加了拦截之类的:
//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;
// 处理第一次ACTION_DOWN事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 当开始一个新的触摸手势时,扔掉所有以前的状态。
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 检查是否需要拦截
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); // 恢复操作,防止被更改
} else {
intercepted = false;
}
} else {
// 没有目标来处理该事件,而且也不是一个新的事件事件(ACTION_DOWN), 进行拦截。
intercepted = true;
}
// 判断事件是否是针对可访问的焦点视图
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 检查事件是否被取消(ACTION_CANCEL)
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// 如果需要,更新指针向下的触摸目标列表
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 如果事件是针对可访问性焦点视图,我们将其提供给具有可访问性焦点的视图。
// 如果它不处理它,我们清除该标志并像往常一样将事件分派给所有的 ChildView。
// 我们检测并避免保持这种状态,因为这些事非常罕见。
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;
// 清除此指针ID的早期触摸目标,防止不同步。
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);
// 查找可以接受事件的 ChildView
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//遍历所有子 View,查找可以接收事件的子View。
//从后往前查找,也遵循了覆盖在目标View的上方点击,先分发给最上层的View
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)) {
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
//根据触摸位置找到目标View,则直接跳出当前循环 break
// 孩子已经在自己的范围内受到了触碰
// 除了它正在处理的指针之外,还给它一个新指针
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
//重置状态
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 孩子想在自己的范围内得到事件
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex指向预先排序的列表,找到原始索引
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;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// // 没有找到 ChildView 接收事件
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 {
// 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;
}
}
// 如果需要,更新指针向上或取消的触摸目标列表。
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;
}
实际上上面的源码可能我们很多都不需要关注,重要的点在于:
1、判断是否需要拦截,拦截则直接调用自己的onTouchEvent方法,没有拦截则交给子 View,子View是否消费,如果没有消费则回调自身的onTouEvent方法
2、自身是否需要这个事件,如果不需要则询问子 View是否需要,如果不需要则回调自身的onTouchEvent方法
3、子 View有很多,通过循环遍历获取子 View,手指触摸点的位置是哪个子 View,就把事件分发给它,如果当前位置下有多个View重叠,则从上往下看是否需要事件(是否注册了事件或设置了clickable=true)
4、通过第三点也可以知道,只要设置clickable=true(设置可点击,不会给下层再分发事件),则事件就会被消费。