View 的事件体系
View 基础知识
ViewGroup
ViewGroup 可被翻译为控件组,可理解为 View 的组合,它可以包含很多 View 以及 ViewGroup,它也继承自 View,比如 LinearLayout、RelativeLayout 等控件都是继承自 ViewGroup。
当手指触摸屏幕上时,手指所在的区域既能在 ViewGroup 显示范围内,也可能在其内部 View 控件上。因此它内部的事件分发的重心是处理当前 Group 和子 View 之间的逻辑关系:
当前 Group 是否需要拦截 touch 事件;
是否需要将 touch 事件继续分发给子 View;
如何将 touch 事件分发给子 View。
View
View 是 Android 中所有控件的基类,是一种界面层的控件的一种抽象,它代表了一个控件,比如 TextView、ImageView 等控件都是继承自 View。
View 是一个单纯的控件,不能再被细分,内部也并不会存在子 View,所以它的事件分发的重点在于当前 View 如何去处理 touch 事件,并根据相应的手势逻辑进行一些列的效果展示(比如滑动,放大,点击,长按等)。
是否存在 TouchListener;
是否自己接收处理 touch 事件(主要逻辑在 onTouchEvent 方法中)。
坐标系
Android 系统中有两种坐标系,分别是 Android 坐标系和 View 坐标系。
Android 坐标系
在 Android 中,将屏幕左上角的顶点作为原点,向右方向是 X 轴正方向,向下是 Y 轴正方向。另外在触控事件中,使用 getRawX() 和 getRawY() 方法获得的坐标也是 Android 坐标系的坐标。
VIew 坐标系
View 坐标系与 Android 坐标系共同存在。
View 的位置参数,View 的位置主要由它的四个顶点来决定,分别对应 View 的四个属性:
它们是相对坐标,相对父容器来说,在 Android 中,x 轴和 y 轴的正方向分别为右和下。
- top:左上角纵坐标,getTop()。
- left:左上角横坐标,getLeft()。
- right:右下角横坐标,getRight。
- bottom:右下角纵坐标,getBottom()。
- width = right - left(系统也提供了 getWidth() 用来获取 View 自身宽度,源码中 mRight - mLeft )
- height = bottom - top(系统也提供了 getHeight() 用来获取 View 自身高度,源码中 mBottom - mTop )
注意:View 在平移的过程中,top 和 left 表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是 x、y、translationX 和 translationY 这四个参数:
它们也是相对于父容器的坐标,View 也为它们提供了 get/set 方法。
- x、y:View 左上角坐标。
- translationX、translationY:View 左上角相对于父容器的偏移量,默认值是0。
- y = top + translationY
MotionEvent 和 TouchSlop
MotionEvent
无论是 View 还是 ViewGroup,最终的点击事件都会由 onTouchEvent(MotionEvent event) 方法来处理。MotionEvent 相关于手指接触屏幕后所产生的一系列事件。同时,通过 MotionEvent 对象可以得到点击事件发生的 x 和 y 坐标:
- getX/getY:返回触摸点相对于当前 View 左上角的坐标(视图坐标)。
- getRawX/getRawY:返回触摸点相对于手机屏幕左上角的坐标(绝对坐标)。
TouchSlop
它是系统所能识别出的被认为是滑动的最小距离。这是一个常量,和设备有关,不同设备上的值可能不同。
1 | // 获取这个常量 |
VelocityTracker、GestureDetector 和 Scroller
VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
1 | // 首先,在 View 的 onTouchEvent 方法中追踪当前单击事件的速度。 |
GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
1 | // 创建 GestureDetector 对象并实现 OnGestureListener 或 OnDoubleTapListener 接口 |
Scroller
弹性滑动对象,用于实现 View 的弹性滑动。
1 |
|
View 的滑动
基本思想:当点击事件传到 View 时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改 View 的坐标。实现 View 滑动有很多种方法,比如:layout()、offsetLeftAndRight()、offsetTopAndBottom()、LayoutParams、动画、scrollTo 与 scrollBy,以及 Scroller。
layout() 方法
View 进行绘制时会调用 onLayout 方法来设置显示的位置,因此我们同样可通过修改 View 的 left、top、right、bottom 这 4 种属性来控制 View 的坐标。
1 | // 随手指移动 |
offsetLeftAndRight() 与 offsetTopAndBottom()
与 layout() 方法的效果和使用方式都差不多,修改 ACTION_MOVE 中代码。
1 | case MotionEvent.ACTION_MOVE: |
使用 scrollTo/scrollBy
这是 View 本身提供的方法,它们只能改变 View 内容的位置而不是 View 在布局中的位置:
- scrollTo(x,y):表示移动到一个具体的坐标点。
- scrollBy(dx,dy):表示移动的增量为 dx 和 dy。(内部最终也是调用了 scrollTo)
View 内部的两个属性,它们的单位是像素,可通过 getScrollX 和 getScrollY 得到:
- mScrollX:
它的值总是等于 View 左边缘和 View 内容左边缘在水平方向上的距离
从左向右滑动,为负值,反之为正值。 - mScrollY:
它的值总是等于 View 上边缘和 View 内容上边缘在竖直方向上的距离
从上向下滑动,为负值,反之为正值。
1 | case MotionEvent.ACTION_MOVE: |
使用动画
主要是操作 View 的 translationX 和 translationY 属性,既可以采用传统的 View 动画,也可以采用属性动画。
注意:
View 动画是对 View 的影像做操作它并不能真正改变 View 的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将 fillAfter 属性设置为 true,否则动画完成后其动画效果会消失。
而属性动画并不会存在上述问题,但在 Android 3.0 以下无法使用属性动画,需要使用动画兼容库。
改变布局参数
LayoutParams 主要保存了一个 View 的布局参数,因此可通过 LayoutParams 来改变 View 的布局参数从而达到改变 View 位置的效果。
1 | case MotionEvent.ACTION_MOVE: |
除了使用布局的 LayoutParams 外,还可以使用 ViewGroup.MarginLayoutParams 来实现。
1 | case MotionEvent.ACTION_MOVE: |
各种滑动方式对比
- scrollTo/scrollBy:操作简单,适合对 View 内容的滑动
- 动画:操作简单,主要适用于没有交互的 View 和实现复杂的动画效果
- 改变布局参数:操作稍微复杂,适用于有交互的 View
使用属性动画,实现一个跟手滑动的效果:
1 |
|
弹性滑动
本质就是将一次大的滑动分成若干次小的滑动,并在一段时间内完成。
使用 Scroller
区别于 scrollTo 和 scrollBy 方法实现滑动时的过程是瞬间完成的,可使用 Scroller 来实现有过渡效果的滑动。
Scroller 本身是不能实现 View 的滑动,它需要与 View 的 computeScroll() 方法配合才能实现弹性滑动的效果。
1 | public class CustomView extends View { |
- startScroll:只是保存了传递的几个参数,并没有做滑动相关的事。
- invalidate:通过它来实现的滑动。
它会导致 View 重绘,在 View 的 draw 方法中会调用 computeScroll 方法(空实现,要自己实现)。 - computeScroll:
它会向 Scroller 获取当前的 scrollX 和 scrollY,并通过 scrollTo 方法实现滑动,接着调用 postInvalidate 方法进行二次重绘。此次重绘和第一次一样,还是会导致 computeScroll 方法被调用,从而反复执行这个流程。 - computeScrollOffset:它会根据时间的流逝来计算出当前的 scrollX 和scrollY 的值。
返回值 true,表示滑动还未结束。
注:invalidate 与 postInvalidate 的区别:
- invalidate 须在 UI 线程调用。
- postInvalidate 可在工作线程中调用。
通过动画
动画本身就是一种渐进的过程。
使用延时策略
核心思想是通过发送一系列延时消息从而达到一种渐进式的效果。
但采用这种方式无法精确的定时,因为系统的消息调度也是需要时间的,并且时间不定。
View 的事件分发机制
Android touch 事件的分发是 Android 工程师必备技能之一。关于事件分发主要有几个方向可以展开深入分析:
touch 事件是如何从驱动层传递给 Framework 层的 InputManagerService;
WMS 是如何通过 ViewRootImpl 将事件传递到目标窗口;
touch 事件到达 DecorView 后,是如何一步步传递到内部的子 View 中的。
其中与上层软件开发息息相关的就是第 3 条,也是重点关注内容。
点击事件用 MotionEvent 来表示,当一个点击事件产生后,事件最先传递给 Activity 。Activity 包含一个 Window 对象,这个对象是由 PhoneWindow 来实现的。PhoneWindow 将 DecorView 作为整个应用窗口的根 View,而这个 DecorView 又将屏幕划分为两个区域:TitleView 和 ContentView,平常做应用所写的布局正是展示在 ContentView 中的。
点击事件的传递规则
当点击屏幕时便产生了点击事件,这个事件被封装成了一个类:MotionEvent。当 MotionEvent 产生后,系统就会将它传递给 View 的层级,MotionEvent 在 View 中的层级传递过程就是点击事件分发。
事件分发,分析的对象就是 MotionEvent,它的分发过程由三个很重要的方法共同完成:
- public boolean dispatchTouchEvent(MotionEvent event):用来进行事件的分发。
如果事件能够传递给当前 View,那么方法一定会被调用,返回结果受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。 - public boolean onInterceptTouchEvent(MotionEvent event):用来进行事件的拦截。
在上述方法内部调用,用来判断是否拦截某个事件,如果当前 ViewGroup 拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。(需要注意的是 View 没有提供该方法) - public boolean onTouchEvent(MotionEvent event):用来处理点击事件
在 dispatchTouchEvent 方法中调用,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
整个 View 之间的事件分发,实质上就是一个大的递归函数,而这个递归函数就是 dispatchTouchEvent 方法。在这个递归的过程中会适时调用 onInterceptTouchEvent 来拦截事件,或者调用 onTouchEvent 方法来处理事件。
它们的关系可以用如下伪代码表示:
1 | /** |
onInterceptTouchEvent 方法和 onTouchEvent 方法都在 dispatchTouchEvent 方法中调用。接下来根据这段伪代码分析一下点击事件分发的传递规则。
首先点击事件由上而下的传递规则,当点击事件产生后会由 Activity 来处理,传递给 PhoneWindow,再传递给 DecorView,最后传递给顶层的 ViewGroup。一般在事件传递中只考虑 ViewGroup 的 onInterceptTouchEvent 方法,因为一般情况下我们不会重写 dispatchTouchEvent 方法。对于根 ViewGroup,点击事件首先传递给它的 dispatchTouchEvent 方法,如果该 ViewGroup 的 onInterceptTouchEvent 方法返回 true,则表示它要拦截这个事件,这个事件就会交由它的 onTouchEvent 方法处理,如果 onInterceptTouchEvent 方法返回 false,则表示它不拦截这个事件,则这个事件会交给它的子元素的 dispatchTouchEvent 方法来处理,如此反复下去。如果传递给底层的 View,View 是没有子 View 的,就会调用 View 的 dispatchTouchEvent 方法,一般情况下最终会调用 View 的 onTouchEvent 方法。(事件由上而下传递返回值的规则:为 true,则拦截,不继续向下传递,为 false,则不拦截,继续向下传递。)
点击事件由下而上的传递。当点击事件传递底层的 View 时,如果其 onTouchEvent 方法返回 true,则事件由底层的 View 消耗并处理,如果返回 false 则表示该 View 不做处理,则传递给父 View 的 onTouchEvent 处理,如果父 View 的 onTouchEvent 依旧返回 false,则继续传递给该父 View 的父 View 处理,如此反复下去。(事件由下而上传递返回值的规则:为 true,则处理了,不继续向上传递,为 false,则不处理,继续向上传递。)
当一个 View 需要处理事件时:
onTouchListener、onTouchEvent()、onClickListener 的先后顺序:
- 当它设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch 方法会被回调。
- 如果 onTouch 的返回值为 false,那么 View 的 onTouchEvent 会被调用,true 则不会调用。
- 给 View 设置的 OnTouchListener,其优先级比 onTouchEvent 要高。
- 在 onTouchEvent 中如果设置了 OnClickListener,那么它的 onClick 方法会被调用。
- OnClickListener 的优先级最低,即处于事件传递的尾端。
当一个点击事件产生后,传递过程如下:
针对 ACTION_DOWN:
如果没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从Activity—->ViewGroup—>View 从上往下调用dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由View—>ViewGroup—>Activity从下往上调用onTouchEvent方法。
dispatchTouchEvent 和 onTouchEvent 一旦return true(事件被消费了),代表事件停止传递了(到达终点)(没有谁能再收到这个事件)。
dispatchTouchEvent 和 onTouchEvent ,return false 的时候事件都回传给父控件的onTouchEvent处理。
- 对于dispatchTouchEvent 返回 false 的含义应该是:事件停止往子View传递和分发同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传直到某个onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。
- 对于onTouchEvent return false 就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。
dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent
ViewGroup 和View的这些方法的默认实现就是会让整个事件按照U型完整走完,所以 return super.xxxxxx() 就会让事件依照U型的方向的完整走完整个事件流动路径),中间不做任何改动,不回溯、不终止,每个环节都走到。onInterceptTouchEvent 的作用
Intercept 的意思就拦截,每个ViewGroup每次在做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理)如果要自己处理那就在onInterceptTouchEvent方法中 return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()和return false是一样的,是不会拦截的,事件会继续往子View的dispatchTouchEvent传递。
ViewGroup 和 View 的dispatchTouchEvent方法返回super.dispatchTouchEvent()的时候事件流走向。
- 首先看下ViewGroup 的dispatchTouchEvent,之前说的return true是终结传递。return false 是回溯到父View的onTouchEvent,然后ViewGroup怎样通过dispatchTouchEvent方法能把事件分发到自己的onTouchEvent处理呢,return true和false 都不行,那么只能通过Interceptor把事件拦截下来给自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent,记住这一点。
- 那么对于 View 的dispatchTouchEvent return super.dispatchTouchEvent()的时候呢事件会传到哪里呢,很遗憾View没有拦截器。但是同样的道理return true是终结。return false 是回溯会父类的onTouchEvent,怎样把事件分发给自己的onTouchEvent 处理呢,那只能return super.dispatchTouchEvent,View类的dispatchTouchEvent()方法默认实现就是能帮你调用View自己的onTouchEvent方法的。
ViewGroup 和 View 的dispatchTouchEvent 是做事件分发,那么这个事件可能分发出去的四个目标:
注:——> 后面代表事件目标需要怎么做。
1、 自己消费,终结传递。——->return true ;
2、 给自己的onTouchEvent处理——-> 调用super.dispatchTouchEvent()系统默认会去调用 onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的onTouchEvent处理。
3、 传给子View——>调用super.dispatchTouchEvent()默认实现会去调用 onInterceptTouchEvent 在onInterceptTouchEvent return false,就会把事件传给子类。
4、 不传给子View,事件终止往下传递,事件开始回溯,从父View的onTouchEvent开始事件从下到上回归执行每个控件的onTouchEvent——->return false;
注: 由于View没有子View所以不需要onInterceptTouchEvent 来控件是否把事件传递给子View还是拦截,所以View的事件分发调用super.dispatchTouchEvent()的时候默认把事件传给自己的onTouchEvent处理(相当于拦截),对比ViewGroup的dispatchTouchEvent 事件分发,View的事件分发没有上面提到的4个目标的第3点。
ViewGroup和View的onTouchEvent方法是做事件处理的,那么这个事件只能有两个处理方式:
1、自己消费掉,事件终结,不再传给谁—–>return true;
2、继续从下往上传,不消费事件,让父View也能收到到这个事件—–>return false;View的默认实现是不消费的。所以super==false。
ViewGroup的onInterceptTouchEvent方法对于事件有两种情况:
1、拦截下来,给自己的onTouchEvent处理—>return true;
2、不拦截,把事件往下传给子View—->return false,ViewGroup默认是不拦截的,所以super==false;
关于ACTION_MOVE 和 ACTION_UP:
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
如果在某个控件的dispatchTouchEvent 返回true消费终结事件,那么收到ACTION_DOWN 的函数也能收到 ACTION_MOVE和ACTION_UP。
如果在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。
Android 中 touch 事件的传递机制是怎样的:
1、Touch事件传递的相关 API 有 dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent。
2、Touch事件相关的类有View、ViewGroup、Activity。
3、Touch事件会被封装成MotionEvent对象,该对象封装了手势按下、移动、松开等动作。
4、Touch事件通常从Activity#dispatchTouchEvent发出,只要没有被消费,会一直往下传递,到最底层的View。
5、如果Touch事件传递到的每个View都不消费事件,那么Touch事件会反向向上传递,最终交由Activity#onTouchEvent处理。
6、onInterceptTouchEvent为ViewGroup特有,可以拦截事件。
7、Down事件到来时,如果一个View没有消费该事件,那么后续的MOVE/UP事件都不会再给它。
事件传递机制的一些结论:
- 同一个事件序列从手指接触屏幕的那一刻起(down 事件开始,中间含有不定数的 move 事件),到手指离开屏幕的那一刻结束(up 事件结束)。
- 一般一个事件序列只能被一个 View 拦截且消耗,但它可以通过 onTouchEvent 强行传递给其他 View 处理。
- 当 View 决定拦截事件,则这一个事件序列都只能有它来处理,并且 onInterceptTouchEvent 不会再被调用。
- 当 View 开始处理事件,如果不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理(父元素的 onTouchEvent 被调用)。
- 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
- ViewGroup 默认不拦截任何事件。
- View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么 onTouchEvent 方法会被调用。
- View 的 onTouchEvent 默认都会消耗事件(返回 ture),除非它是不可点击的(clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认为 false,clickable 属性要分情况,Button 的默认为 true,而 TextView 默认为 false。
- View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
- onClick 会发生的前提是当前 View 是可点击的,并且它收到了 down 和 up 的事件。
- 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子 View,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。
事件分发的源码解析
点击事件产生后,首先传递给当前的 Activity,这会调用 Activity 的 dispatchTouchEvent 方法,当然具体的事件处理工作都是交由 Activity 中的 PhoneWindow 来完成的,然后 PhoneWindow 再把事件处理工作交给 DecorView,之后再由 DecorView 将事件处理工作交给根 ViewGroup。首先查看 ViewGroup 的 dispatchTouchEvent 方法,其主要分为 3 大步骤。
步骤 1:判断当前 ViewGroup 是否需要拦截此 touch 事件,如果拦截则此次 touch 事件不再会传递给子 View(或者以 CANCEL 的方式通知子 View)。
步骤 2:如果没有拦截,则将事件分发给子 View 继续处理,如果子 View 将此次事件捕获,则将 mFirstTouchTarget 赋值给捕获 touch 事件的 View。
步骤 3:根据 mFirstTouchTarget 重新分发事件。
1 |
|
当 ViewGroup 要拦截事件时,那么后续的事件序列都将交给他处理,而不用再调用 onInterceptTouchEvent 方法了。所以,onInterceptTouchEvent 方法并不是每次事件都会调用的。onInterceptTouchEvent 方法默认返回 false,不进行拦截。如果想要让 ViewGroup 拦截事件,那么应该在自定义的 ViewGroup 中重写这个方法。
1 | public boolean onInterceptTouchEvent(MotionEvent ev) { |
接着查看 ViewGroup 的 dispatchTouchEvent 方法。
1 |
|
容易被遗漏的 CANCEL 事件,查看 dispatchTransformedTouchEvent 方法。
1 | private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, |
当传入的参数 cancel 为 true,并且 child 不为 null,最终这个事件会被包装为一个 ACTION_CANCEL 事件传给 child。什么情况下会触发这段逻辑呢?
总结一下就是:当父视图的 onInterceptTouchEvent 先返回 false,然后在子 View 的 dispatchTouchEvent 中返回 true(表示子 View 捕获事件),关键步骤就是在接下来的 MOVE 的过程中,父视图的 onInterceptTouchEvent 又返回 true,intercepted 被重新置为 true,此时上述逻辑就会被触发,子控件就会收到 ACTION_CANCEL 的 touch 事件。
实际上有个很经典的例子可以用来演示这种情况:
当在 Scrollview 中添加自定义 View 时,ScrollView 默认在 DOWN 事件中并不会进行拦截,事件会被传递给 ScrollView 内的子控件。只有当手指进行滑动并到达一定的距离之后,onInterceptTouchEvent 方法返回 true,并触发 ScrollView 的滚动效果。当 ScrollView 进行滚动的瞬间,内部的子 View 会接收到一个 CANCEL 事件,并丢失touch焦点。
因此,我们平时自定义View时,尤其是有可能被ScrollView或者ViewPager嵌套使用的控件,不要遗漏对CANCEL事件的处理,否则有可能引起UI显示异常。
总结
dispatchTouchEvent 的事件的流程机制,这一过程主要分 3 部分:
判断是否需要拦截 —> 主要是根据 onInterceptTouchEvent 方法的返回值来决定是否拦截;
在 DOWN 事件中将 touch 事件分发给子 View —> 这一过程如果有子 View 捕获消费了 touch 事件,会对 mFirstTouchTarget 进行赋值;
最后一步,DOWN、MOVE、UP 事件都会根据 mFirstTouchTarget 是否为 null,决定是自己处理 touch 事件,还是再次分发给子 View。
整个事件分发中的几个特殊的点。
DOWN 事件的特殊之处:事件的起点;决定后续事件由谁来消费处理;
mFirstTouchTarget 的作用:记录捕获消费 touch 事件的 View,是一个链表结构;
CANCEL 事件的触发场景:当父视图先不拦截,然后在 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。
ViewGroup 是继承 View 的,接下来查看 View 的 dispatchTouchEvent(event) 方法。
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
在 onTouchEvent 方法内部,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一个为 true,那么 onTouchEvent 就会返回 true 消费这个事件。CLICKABLE 和 LONG_CLICKABLE 代表 View 可以被点击和长按点击,可通过 View 的 setClickable 和 setLongClickable 方法来设置,也可以通过 View 的 setOnCLickListener 和 setOnLongClickable 来设置,它们会自动将 View 设置为 CLICKABLE 和 LONG_CLICKABLE。
接着在 ACTION_UP 事件中会调用 performClick 方法。 此方法中,如果 View 设置了点击事件 OnClickListener,那么它的 onClick 方法就会被执行。
Activity 对点击事件的分发过程
事件交给 Activity 所属的 Window 进行分发,返回 true 则整个事件循环结束,返回 false 则意味着事件没人处理,所有 View 的 onTouchEvent 都返回 false,则 Activity 的 onTouchEvent 会被调用。
Window 对点击事件的分发过程
Window 的实现类 PhoneWindow 将事件直接传递给了 DecorView,而 DecorView 继承自 FrameLayout 且是父 View,所以最终事件会传递给顶级 View(根 View,一般是 ViewGroup)。
顶级 View 对点击事件的分发过程
点击事件到达顶级 View,并调用它的 dispatchTouchEvent 方法。
如果它拦截事件,则 onInterceptTouchEvent 返回 ture。
这时如果它的 mOnTouchEvent 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。
在 onTouchEvent 中,如果设置了 mOnClickListener ,则 onClick 会被调用。
如果它不拦截事件,则事件会传递给它所在的点击事件链上的子 View。
View 对点击事件的处理过程
这里的 View 不包含 ViewGroup。所以也无需向下传递事件。
View 的滑动冲突
在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。
Android 下解决滑动冲突的常见思路是什么?
相关的滑动组件重写 onInterceptTouchEvent,然后判断根据 xy 值,来决定是否要拦截当前操作。
常见的滑动冲突场景
- 内外滑动方向不一致
这种一般是 ViewPager 和 Fragment 配合使用的效果。但 ViewPager 相较于 ScrollView 等,其内部处理了这种冲突。 - 内外滑动方向一致
- 上面两种情况的嵌套
虽看似复杂,但就是几个单一的滑动冲突的叠加,只需分别处理内层和中层,中层和外层之间的冲突即可。
滑动冲突的处理规则
- 外部左右滑动,内部上下滑动
判断滑动方向,当用户左右滑动时,由外部 View 拦截点击事件,当上下滑动时,由内部 View 拦截。 - 可依据滑动路径和水平方向所形成的夹角判断方向
- 可依据水平和竖直方向上的距离差判断滑动方向
- 可依据水平和竖直方向的速度差判断滑动方向
- 内外方向一致
根据业务需要处理 - 嵌套情况
根据业务需要处理
滑动冲突的解决办法
外部拦截法
指点击事件都先经过父容器的拦截处理,需重写父容器的 onInterceptTouchEvent 方法,在内部做相应拦截。
1 | /** |
内部拦截法
指父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则交由父容器处理,需配合 requestDisallowInterceptTouchEvent 方法,和重写子元素的 dispatchTouchEvent 方法。
1 | /** |
链接
参考资料:
Android 开发艺术探索
传送门:GitHub