事件机制
由View的加载一文得知View并不是我们真正看到的控件,而只是指导Canvas绘制的“导演”使用View的measure、layout和draw三个方法,分别确定View的大小、位置以及View的外观样式。由于Canvas只能控制控件最后是啥样的,一般时候,我们使用控件,不仅需要其展示在界面上,经常我们还需要这些控件与用户进行交互,比如点击、滑动、拖拽等等。从View的加载绘制源码可以看出,Canvas是没有处理事件的能力的,因此事件处理也是View这个导演的一个能力了。
一般来说事件机制有两大类,分别是冒泡和捕获。更确切来说事件都是先捕获再冒泡。JavaScript中的事件监听是addEventListener(event, function, useCapture)其中event参数表示事件名,function表示事件处理函数,useCapture表示是否在捕获阶段执行。如果有多个View相互重叠,里面的View相当于外层View的子View,子View被点击了,可以理解为外层View也被点击了,但如果多层View都注册了事件处理监听,那么最后应该谁来处理这个事件,或者多个事件监听都要处理,就得有一套完善的机制。事件一般都是从最外层,也就是整个页面对象来监听到的,然后一层层向内传递,这个过程叫捕获,如果JS的useCapture的值为ture,多个事件处理函数的执行顺序就是从外到内的。事件传递到最内部的一个View后,会再向外抛,这个过程叫冒泡。执行顺序与捕获相反。JS可以通过useCapture来决定是在捕获还是冒泡阶段执行事件处理函数,而Android要更简洁一点,Android的事件机制是基于捕获的,没有冒泡过程。

Android事件中的一些重要对象
首先是最重要的事件类MotionEvent,表示手指接触屏幕后所产生的一系列事件,有三种类型:
- ACTION_DOWN 手指刚接触屏幕
- ACTION_MOVE 手指在屏幕上移到
- ACTION_UP 手指从屏幕上松开的一瞬间
点击屏幕的过程是ACTION_DOWN --> ACTION_UP
在屏幕上滑动的过程是ACTION_DOWN --> ACTION_MOVE --> ACTION_UP
MotionEvent提供了非常多的方法,可以获取到事件的很多参数,其中最重要的就是获取点击位置。有getX/getY和getRawX/getRawY两种,分别表示基于当前View左上角的x和y坐标以及基于手机屏幕左上角的x和y坐标。
再者是TouchSlop对象,表示被系统能识别的滑动的最小距离。
VelocityTracker用于计算事件过程中手指的滑动事件,使用方式是
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null) {
return super.onTouchEvent(event)
}
if (null == velocityTracker) {
velocityTracker = VelocityTracker.obtain()
} else {
velocityTracker?.recycle()
velocityTracker = VelocityTracker.obtain()
}
velocityTracker?.let {
it.addMovement(event);
when (event.action) {
MotionEvent.ACTION_UP -> {
// 隐藏在左边的宽度
Log.e(TAG, "V=" + it.xVelocity)
if (Math.abs(it.xVelocity) > 4000f) {
if (it.xVelocity < 0f) {
//正向逻辑代码
} else {
//反向逻辑代码
}
}
}
MotionEvent.ACTION_MOVE ->
it.computeCurrentVelocity(1000) //设置units的值为1000,意思为一秒时间内运动了多少个像素
}
}
return super.onTouchEvent(event)
}
首先获取VelocityTracker对象,将事件添加进去,再调用计算函数,传入的是计算时间,1000表示要计算1000ms的事件,最后得到两个方向上的速度,分别表示1000ms移到的像素。最后需要将VelocityTracker的对象回收。
GestureDetector对象主要用于接管事件处理并自动帮我们识别常见的单击、滑动、长按、双击等事件。
Android事件分发机制
重写Activity的dispatchTouchEvent方法,用Exception的printStackTrace将当前调用栈打出来如下
2020-07-24 11:01:50.076 24995-24995/? I/System.out: java.lang.RuntimeException
2020-07-24 11:01:50.078 24995-24995/? I/System.out: at com.benson.android.aidl.client.BookClientActivity.dispatchTouchEvent(BookClientActivity.kt:97)
2020-07-24 11:01:50.079 24995-24995/? I/System.out: at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:508)
2020-07-24 11:01:50.080 24995-24995/? I/System.out: at android.view.View.dispatchPointerEvent(View.java:13822)
2020-07-24 11:01:50.082 24995-24995/? I/System.out: at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5736)
2020-07-24 11:01:50.083 24995-24995/? I/System.out: at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5519)
2020-07-24 11:01:50.084 24995-24995/? I/System.out: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4992)
2020-07-24 11:01:50.085 24995-24995/? I/System.out: at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5055)
2020-07-24 11:01:50.086 24995-24995/? I/System.out: at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5016)
2020-07-24 11:01:50.087 24995-24995/? I/System.out: at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5166)
2020-07-24 11:01:50.088 24995-24995/? I/System.out: at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5024)
2020-07-24 11:01:50.089 24995-24995/? I/System.out: at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5223)
2020-07-24 11:01:50.090 24995-24995/? I/System.out: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4992)
2020-07-24 11:01:50.091 24995-24995/? I/System.out: at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5055)
2020-07-24 11:01:50.092 24995-24995/? I/System.out: at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5016)
2020-07-24 11:01:50.093 24995-24995/? I/System.out: at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5024)
2020-07-24 11:01:50.094 24995-24995/? I/System.out: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4992)
2020-07-24 11:01:50.095 24995-24995/? I/System.out: at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7769)
2020-07-24 11:01:50.096 24995-24995/? I/System.out: at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7738)
2020-07-24 11:01:50.098 24995-24995/? I/System.out: at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7695)
2020-07-24 11:01:50.099 24995-24995/? I/System.out: at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7898)
2020-07-24 11:01:50.100 24995-24995/? I/System.out: at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:206)
2020-07-24 11:01:50.101 24995-24995/? I/System.out: at android.os.MessageQueue.nativePollOnce(Native Method)
2020-07-24 11:01:50.102 24995-24995/? I/System.out: at android.os.MessageQueue.next(MessageQueue.java:340)
2020-07-24 11:01:50.103 24995-24995/? I/System.out: at android.os.Looper.loop(Looper.java:183)
2020-07-24 11:01:50.104 24995-24995/? I/System.out: at android.app.ActivityThread.main(ActivityThread.java:7742)
2020-07-24 11:01:50.104 24995-24995/? I/System.out: at java.lang.reflect.Method.invoke(Native Method)
2020-07-24 11:01:50.105 24995-24995/? I/System.out: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:508)
2020-07-24 11:01:50.106 24995-24995/? I/System.out: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034)
可见最终的调用处是native代码,Activity#dispatchTouchEvent的实现如下
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
如果是按下操作就执行onUserInteraction方法,Activity的onUserInteraction是个空方法。然后判断getWindow().superDispatchTouchEvent,如果返回true,就直接return ture,否则调用onTouchEvent。看注释,这里的superDispatchTouchEvent的return是window是否消费这个事件,不消费就看Activity是否消费,Activity用onTouEvent方法来消费。由前面的Activity启动过程可知,这里的getWindow方法返回的是PhoneWindow的对象。PhoneWindow#superDispatchTouchEvent方法中直接调用了mDecor.superDispatchTouchEvent方法,而DecorView#superDispatchTouchEvent直接调用了其dispatchTouchEvent方法。DecorView是个FrameLayout,并没有重写ViewGroup的dispatchTouchEvent,ViewGroup#dispatchTouchEvent的逻辑比较复杂,可以简要理解成如下代码。
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchEvent != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT != 0;
if (!disallowIntecept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
if (!canceled && !intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getAndVerifyPreorderdView(...)
// 判断child是否可见,并且点击的位置位于这个View的范围内
if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
......
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) {
......
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
}
}
这里其实就是遍历所有的子View,如果当前的点击位置不在子View的范围内就continue,否则就调用dispatchTransformedTouchEvent,如果dispatchTransformedTouchEvent返回true,表示这个子View消费了当前事件,就直接break。这里的dispatchTransformedTouchEvent会调用传入的子View的dispatchTouchEvent,这样就实现了一个递归调用,直接点击范围内最深处的子View处理这个事件,以实现捕获事件。
这里有几个非常重要的点:
- 判断actionMasked为ACTION_DOWN或者mFirstTouchTarget不为null时才执行onInterceptTouchEvent
- 判断disallowIntercept的值,这个值是用requestDisallowInterceptTouchEvent方法来设置的
- 用onInterceptTouchEvent来判断当前View是否要拦截事件,拦截了事件就不会分发给子View了
- 如果子View消费了事件,就调用addTouchTarget,查看addTouchTarget的代码,其实mFirstTouchTarget是一个链表,addTouchTarget的调用只是将view添加到链表的表头
由这四点,可以得到如下结论:
- 根据1和4可以判断出,如果所有的子View都没有消费ACTION_DOWN事件,那么后续的事件就不会再分发了,而是当前ViewGroup直接消费。
- 根据2可知可通过调用requestDisallowInterceptTouchEvent方法来让当前ViewGroup不拦截事件,从而让子View可以被分发到
- 由3可知当前ViewGroup可通过onInterceptTouchEvent方法来判断是否要拦截事件
这上面是ViewGroup的事件分发机制,而最终事件都会由View的dispatchTouchEvent处理。View的dispatchTouchEvent虽不像ViewGroup那样有子View的分发逻辑那么复杂,但代码也有50多行,主要逻辑如下
if ((mViewFlags & ENABLE_MASK) == ENABLED && handleScrllBarDragging(event)) {
result = true;
}
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;
}
这里实现会先判断这个View的OnTOuchListener是否有被set并且当前View是否是enable状态,如果为true就调用OnTouchListener的onTouch方法,这个onTouch方法如果返回了false,就表示不消费这个事件,再调用View的onTouchEvent方法,View的onTouchEvent的返回值也表示是否消费这个事件。 View的onTouchEvnet的实现代码简化后如下
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
if ((viewFlags & ENABLE_MASK) == DISABLED) {
...
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
switch(action) {
case MotionEvent.ACTION_UP:
......
performClickInternal();
......
break;
}
performClickInternal方法中调用了performClick方法。
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);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
这里取出OnClickListener对象,调用其onClick方法,如果OnClickListener不为null就表示消费了这个事件 从View的dispatchTouchEvent方法的实现来看,View中有三个事件处理,调用顺序分别是OnTouchListener#onTouch、View#onTouch、OnClickListener#onClick。
滑动冲突处理
滑动冲突一般是指当一个可滑动的ViewGroup嵌套着另一个可滑动的ViewGroup时,滑动时两个ViewGroup的事件处理混乱,严重的情况可能会导致两个ViewGroup都滑得非常卡顿。
这里又涉及到几种情况,分别是
- 外部滑动和内部滑动方向不一致
- 外部滑动和内部滑动方向一致
- 第1和第2种的嵌套
一般来说外部滑动和内部滑动方向不一致比较好解决,比如说外部纵向滑动,内部横向滑动,那就在外部的ViewGroup中,判定如果y轴位移大于x轴位置,表示纵向滑动了,由外部ViewGroup消费事件,反之内部ViewGroup消费事件
而当外部和内部滑动的方向一致时,就需要从业务角度寻找突破口。比如侧滑返回上一页和当前的一个横向菜单的滑动冲突了。这时候其实是需要在内部菜单的ViewGroup上做处理的。这种处理方式又叫内部拦截法,可以重写内部ViewGroup的idspatchTouchEvent,判断如果是横向滑动,在ACTION_DOWN处就调用parent.requestDisallowInterceptTouchEvent(true)。ViewGroup的dispatchTouchEvent方法中使用onInterceptTouchEvent方法来判定当前ViewGroup是否需要拦截事件,因此重写OnInterceptTouchEvent,也可解决内外部ViewGroup之间的滑动冲突。这种处理方式又叫外部拦截法。使用外部拦截法需要注意的是onInterceptTouchEvent中ACTION_DOWN时,必须return false,因为ACTION_DOWN是事件系列的第一个事件,如果在ACTION_DOWN时外部ViewGroup就给拦截了,后续的事件也都交由外部ViewGroup处理了,子View就完全没有机会处理这一系列事件了。一般是在ACTION_MOVE中判断是否需要拦截事件,外部需要拦截就return true,否则return false