BAT 高级工程师进阶必修之 View 的事件分发机制
前言
View 在 Android 中是一个非常重要的概念,作用堪比四大组件,所谓 View 的事件分发,就是对 MotionEvent 事件的分发过程,当一个 MotionEvent 产生了以后,系统需要把这个事件传递给一个具体的 View,这个过程就是事件分发
View 的虽称不上 Android 四大组件,但它的重要性可以说是跟四大组件平级,根据使用频率,甚至比广播跟内容提供器重要;view 主要包含两类:ViewGroup 和具体的 View,有 Android 开发经验的都知道这两类的区别了,我们时时刻刻都有使用到 view,例如: TextView、ImageView 等,正因为我们时时刻刻都在用,所以就显得特别重要了
事件分发过程由三个很重要的方法共同来完成:
- public boolean dispatchTouchEvent(MotionEvent ev)
- public boolean onInterceptTouchEvent(MotionEvent ev)
- public boolean onTouchEvent(MotionEvent event)
当一个触摸事件产生后,他的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给 Activity,Activity 再传递给 Window,最后再传给顶级 View,顶级 View 接收到事件后,会按照分发机制向下继续分发
顺着箭头走,可以看到事件是被
- Activity 的 dispatchTouchEvent(MotionEvent ev)方法先拿到的,从方法名可以看出来这个方法就是分发触摸事件的
- 接着会调用 PhoneWindow 的 superDispatchTouchEvent(MotionEvent event)方法,其实 PhoneWindow 是 Window 的唯一实现类
- 再调用 DecorView 的 superDispatchTouchEvent(MotionEvent event)方法,DecorView 其实就是最顶级的 View,继承自 FrameLayout,平时我们熟悉的 setContentView(R.layout.activity_layout)就是将布局设置到这个 DecorView
- 接着来到 ViewGroup ,这个 ViewGroup 就是我们平时自己布局里面的最外层的父布局
例如: LinearLayout,事件来到这里就会稍微复杂起来了,会根据情况处理事件
- 首先还是调用 ViewGroup 的 dispatchTouchEvent(MotionEvent ev)方法,在该方法会优先判断 disallowIntercept 这个布尔值,这个disallowIntercept后面会讲到
-
如果为 true,则说明 ViewGroup 不消费事件,事件就来到了子
View 的 dispatchTouchEvent(MotionEvent event),这里假设我们的布局是很简单的,一个 LinearLayout 包含一个 Button,这样方面分析,所以这个子View就是 Button,如果有设置 mOnTouchListener,这事件会来到mOnTouchListener.onTouch(this, event) - 如果 onTouch 方法返回 true 则该事件就被消费了,返回false的话事件会来到 onTouchEvent(event)方法,在该方法会判断 mOnClickListener 是否被设置,这个监听器是我们平时用的最多的 setOnClickListener(@Nullable OnClickListener l)方法,这个方法的优先级从这里可以看出优先级是最低的,因为事件到最后才会来到这里
- 如果 onTouchEvent(event)返回 false;则会调用 ViewGroup 的 onTouchEvent(ev)
- 如果 ViewGroup 的 onTouchEvent(ev)还是返回 false,则事件最终丢回给 Activity 的 onTouchEvent(ev) 方法了,相当于这个事件走了地球一圈都没人要还是回到了原点
刚刚分析到 ViewGroup 的时候,当 disallowIntercept 为 false 时,事件会来到它的 onInterceptTouchEvent(ev),这个方法的从名字可以看出是否拦截事件的意思,返回 true 表示拦截,则会交给自己的 onTouchEvent(ev)方法,返回 false 表示不拦截事件,将事件丢给了 View 的 dispatchTouchEvent(MotionEvent event),也就相当于 disallowIntercept 等于 true
前面 ViewGroup 说到的 disallowIntercept 变量是通过 parent.requestDisallowInterceptTouchEvent(boolean)设置的,设置是否禁止拦截事件的意思,一般子 view 不希望父类拦截事件的话可以调用该方法,在处理滑动冲突的时候会经常用到这个方法,他的优先级也是最高的
接下来用代码是示例:
Activity 对事件的分发
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity 的事件的处理其实并不负责,即如果下层(不管是 ViewGroup 还是 View )消耗了这个事件,那么 if 语句就为 true , 则 dispatchTouchEvent 就返回 true ;如果没有消耗就自己对事件进行处理,即调用 onTouchEvent 方法
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
Activity 的 onTouchEvent 会对这个事件进行判断,如果事件在窗口边界外就返回 true,dispatchTouchEvent 就返回 true ;如果在边界内就 返回 false ,最后 dispatchTouchEvent 也会返回 false
View 对事件的分发
这里先说 View 对事件的分发是因为 ViewGroup 继承自 View ,ViewGroup 对事件的分发会调用到父类(也就是View )的方法,因此先理清 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;
}
...
return result;
}
可以看到 View 的事件的处理是先判断 mOnTouchListener !=null 和 View 设置 ENABLED 这两个条件成不成立,不过成立则 调用 onTouch 方法,且如果 onTouch 返回了 true ,那个事件就被消耗 ,View 的 dispatchTouchEvent 就返回 true ; 相反,如果条件不成立或者 onTouch 返回 false ,那么就会执行 View 的 onTouchEvent 方法
public boolean onTouchEvent(MotionEvent event) {
...
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 若可点击,包括LONG_CLICKABLE 或者 CLICKABLE
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
...
// 执行performClick()
performClick();
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
} //>> 若可点击,就返回true
return true;
} //>> 若不可点击,就返回false
return false;
}
public boolean performClick() {
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
在 onTouchEvent 方法中,如果 View 是可点击的,比如设置了 onClick 或者 onLongClick ,就会执行 onClick 方法,并且 onTouchEvent 返回 true
- 如果是不可点击的就返回 false 。需要注意的是这里的 onTouchEvent 是可以被重写的
- 如果 onTouchEvent 返回 true 那么 View 的 dispatchTouchEvent 就返回 true ,事件就被消耗
- 如果 onTouchEvent 返回 false , 那么 dispatchTouchEvent 也返回 false ,这时 事件就交由上层处理,也就是 ViewGroup
小结
这篇文章,其实不难;主要是将 View 的事件分发机制以及开发当中经常用到的一些知识点,总结了一下
最后这里放上我耗时两个月,将自己8年 Android 开发的知识笔记整理成了一份系统学习资料笔记,技术相关的知识点在笔记中都有详细的解读并且把每个技术点整理成了 PDF 文档(知识脉络 + 诸多细节)有需要的小伙伴:可点击此处查看获取方式,或者简信发送"笔记"就可以免费领取了
BAT 高工必修- View 绘制
BAT 高工必修- View 事件分发机制
以上就是全套大厂学习笔记面试指南,吃透一半保你可以吊打面试官,只有自己真正强大了,有了核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!开发者们!千里之行,始于足下;种下一颗树最好的时间是十年前,其次,就是现在