Android——View的事件体系
Posted SyubanLiu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android——View的事件体系相关的知识,希望对你有一定的参考价值。
1. View的基础知识
View的基础知识主要有:View的位置参数、MotionEvent、TouchSlop对象、VelocityTracker、GestureDelector和Scroller对象等等。
1.1 什么是View
View是android中所有控件的基类,View可以是单个控件,也可以是多个控件组装起来的一组控件。
1.2 View的位置参数
View的位置由它的四个顶点来决定,分别对应于View的四个属性:top、left、right、bottom,这些属性都是相对于父容器来说的,所以它们是相对坐标。
从Android 3.0 开始,View新增了一些新的参数:x、y、translationX 和 translationY,其中 x 和 y 是View左上角的坐标,translationX 和 translationY 是 View 相对于父容器的偏移量,这几个参数也是相对于父容器的坐标。
注意:View在平移过程中,View的 top 和 left 表示View的原始左上角位置,其值不会发生改变,此时发生改变的是 x、y、translationX、translationY。
1.3 MotionEvent 和 TouchSlop
1.3.1 MotionEvent
手指接触屏幕产生的一系列事件,典型的事件如下几种:
- ACTION_DOWN:手指刚接触到屏幕
- ACTION_MOVE:手指在屏幕上移动
- ACTION_UP:手指从屏幕上松开一瞬间
同时通过MotionEvent对象我们可以获取到点击事件发生的 x 和 y 坐标。系统提供了两组方法:
- getX/getY:返回相对于当前View的左上角的 x 和 y 坐标;
- getRawX/getRawY:返回的是相对于手机屏幕左上角的 x 和 y 坐标;
1.3.2 TouchSlop
TouchSlop 是系统所能识别出的被认为是滑动的最小距离,如果两次滑动之间的距离小于这个常量,那么系统就不认为是在进行滑动操作。这是一个常量,与设别有关,不同设备该常量可能不一样。
可以通过代码获取这个TouchSlop常量值:ViewConfiguration.get(context).getScaledTouchSlop()
应用场景:当需要处理滑动时,可以利用这个常量来做一些过滤,例如两次滑动的距离小于这个值,我们就可以认为未达到滑动的临界值,因此可以认为它们不是滑动。
1.4 VelocityTracker、GestureDelector 和 Scroller
1.4.1 VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度。包括 水平方向的速度 和 垂直方向的速度。
VelocityTracker vt = VelocityTracker.obtain();
vt.addMovement(event);
vt.computeCurrentVelocity(1000);
int xVelocity = (int) vt.getXVelocity();
int yVelocity = (int) vt.getYVelocity();
在获取速度之前:需先计算速度。然后获取到的速度是指一段时间内手指划过的像素数。
当使用完成时,我们需要调用 clear 方法,将其重置并回收内存。
vt.clear();
vt.recycle();
1.4.2 GestureDelector
手势检测,用于辅助检测用户的 单击、滑动、长按、双击 等行为。
在GestureDelector使用过程中,首先需要创建一个GestureDelector对象并实现 OnGestureListener 接口,根据需要我们实现内部对应的接口。接着我们需要接管目标View的 onTouchEvent 方法,在待监听的View的 onTouchEvent 方法中实现:
boolean consume = mGestureDelector.onTouchEvent(event);
returen consume;
在日常开发中,比较常用的有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和 onDoubleTap(双击)。
建议:如果只是监听滑动相关的,建议自己在onTouchEvent中实现,如果要监听双击等这种行为的,那么使用GestureDelector。
1.4.3 Scroller
弹性滑动对象,用于实现View的弹性滑动。当使用View的 scrollTo 或者 scrollBy 进行滑动时,没有过渡动画效果,因此可以使用 Scroller 来实现有过渡效果的滑动。
Scroller 本身是无法进行滑动的,需要 View 的 computeScroll 方法配合使用才能共同完成。
实现原理:通过滑动的百分比,不断绘制View,从而不断调用 View 的 computeScroll 方法来达到滑动的效果。
Scroller mScroller = new Scroller(context);
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
// 重新绘制View
invalidate();
}
@Override
public void computeScroll() {
if(mScroll.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
2. View的滑动
View的滑动可以通过三种方式来实现:
- 通过View本身的 scrollTo / scrollBy 来实现
- 通过动画给View施加平移的效果来实现
- 通过View的 LayoutParams 使得View重新布局从而实现滑动
2.1 使用 scrollTo/scrollBy
scrollBy 实际上也调用了 scrollTo,实现了基于当前位置的相对滑动,而 scrollTo 实现了基于传递参数的绝对滑动。
在滑动的过程中,View内部有两个属性 mScrollX 和 mScrollY,mScrollX 始终等于View的左边缘与View内容左边缘的水平方向的距离,mScrollY 总是等于View的上边缘与View的内容上边缘的竖直方向的距离。
scrollTo 和 scrollBy 只能改变 View内容的位置 而不能改变 View的位置。
2.2 使用动画
主要操作的是View的 translationX 和 translationY 属性。可以采用传统的动画,也可以采用属性动画。
需要注意的一点是,View动画只是对View的影像做操作,并不能改变View的位置参数,包括宽高。若希望动画结束后可以保留当前的状态,则必须将fillAfter属性设置为true,否则动画结束后结果会消失。
注意:使用动画来实现View动画并不能改变View的位置。使用属性动画可以解决该问题。
2.3 改变布局参数
改变布局参数,即改变LayoutParams。
2.4 各种滑动方式的对比
- scrollTo/scrollBy:可以方便地实现滑动效果且不影响内部元素的单击事件,只能滑动View的内容,不能滑动View本身。
- 动画:不能改变View本身的属性,包括宽高。优点用来进行一些复杂的效果动画。
- 改变布局参数:会改变View的本身属性,适用于有交互的View。
3. 弹性滑动
思路:将一次大的滑动分成几次小的滑动并在一个时间段内完成。
实现弹性滑动的方式主流的做法如下:
- 使用Scroller
- 使用Handler#postDelayed
- 使用Thread#Sleep
3.1 Scroller
通过Scroller的滑动指View的内容滑动而不是View本身位置的参数。
Scroller本身是无法实现滑动效果的,需要结合View的 computeScroll 方法来配合完成。通过不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔。
View的每一次重绘都会导致View进行小幅度的滑动,而多个小幅度的滑动就组成了弹性滑动,这就是Scroller的工作原理。
3.2 通过动画
思想其实与Scroller类似,都是通过改变一个百分比配合scrollTo来完成View的重绘。
3.3 使用延时策略
通过 Handler#postDelayed 或者 Thread#sleep 来完成延时策略。
注意:采用Handler来完成延时时,所设定的时间是无法精准地定时,因为系统的消息调度也是需要时间的。
4. View的事件分发机制
通过View的事件分发机制,可以解决View的一大难题——View的滑动冲突。
4.1 点击事件的传递规则
点击事件的传递规则,要分析的对象就是 MotionEvent,即点击事件。
点击事件在分发的过程中,有三个很重要的方法:dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent。
- dispatchTouchEvent:用来进行事件分发。如果当前的事件能够传递给当前的View,那么此方法一定会被调用。表示是否消耗当前事件,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent影响。
- onInterceptTouchEvent:表示是否拦截某个事件,用于ViewGroup中。如果当前View拦截某个事件,那么在同一个事件序列当中,此方法不会再次被调用,返回结果表示是否拦截。
- onTouchEvent:在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前的事件。如果不消耗,那么在同一个事件序列当中,当前View无法再次接收到事件。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
事件传递优先级:OnTouchListener > onTouchEvent > OnClickListener。
- 当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被回调,这时事件的处理还要看onTouch的返回值,如果返回false,那么View的onTouchEvent就会被调用;否则,View的onTouchEvent就不会被调用。
- 在onTouchEvent方法中,如果设置了OnClickListener,那么它的onClick方法就会被调用。
当一个点击事件产生后,传递过程是:Activity -> Window -> View。
4.2 事件传递结论
- 同一个事件序列是指从手指接触屏幕那一刻起,到手指离开屏幕那一刻结束。
- 正常情况下,一个事件序列只能被一个View拦截且消耗。因此同一个事件序列是不可以被两个View同时处理,当然也可通过特殊的手段做到,例如当一个View处理事件时,通过onTouchEvent强行传递给其他的View。
- 某个View一旦决定拦截,那么这个事件序列只能由它处理。并且onInterceptTouchEvent不会再被调用。
- 事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它处理。
- View默认是消耗事件,即返回true。除非它是不可点击的(clickable和longClickable同时为false)。
- View的enable属性onTouchEvent的返回值。只要它的 clickable 或者 longClickable 有一个为true,那么它的onTouchEvent就返回true。
- 事件传递过程是从外向内,即事件先是传递给父元素,然后再由父元素传递给子元素。在子View中,可以通过 requestDisallowInterceptTouchEvent 方法干预父元素的事件分发过程,但是 ACTION_DOW 除外。
4.3 事件分发源码解析
4.3.1 Activity对点击事件的分发过程
当一个点击事件发生时,最先传递给Activity,由Activity的dispatchTouchEvent来进行事件分发,具体工作由Activity内部的Window来完成。
在此过程中,Window的实现类即是 PhoneWindow。Window可以控制顶级View的显示和行为策略。接着PhoneWindow会将当前的事件传递给 DecorView。
我们可以通过 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
来获取Activity所设置的View。DecorView即是视图中顶层的View。
4.3.2 顶级View的点击事件分发过程
ViewGroup在如下两种情况下会判断是否拦截当前事件:
- 事件类型为ACTION_DOWN,点击事件是DOWN时。
- mFirstTarget != null,即当事件由ViewGroup的子元素成功处理时,mFirstTarget 会被赋值并指向子元素,换句话说,就是当ViewGroup不拦截事件并将事件交由子元素处理时 mFirstTarget != null。
FLAG_DISALLOW_INTERCEPT,该标志位是通过 requestDisallowInterceptTouchEvent 来设置的,一般用于子View中。一旦该标志位被设置,那么ViewGroup无法拦截除了ACTION_DOWN以外的点击事件。为什么是ACTION_DOWN以外的事件?因为ACTION_DOWN会重置FLAG_DISALLOW_INTERCEPT,将导致子View中设置这个标志位无效。
注意:
- 当ViewGroup决定拦截事件后,那么后续的点击事件会默认交给它处理并且不再调用它的onInterceptTouchEvent方法。因此onInterceptTouchEvent不是每次都调用的,如果要处理所有事件,选中在dispatchTouchEvent中处理。
- FLAG_DISALLOW_INTERCEPT 该标志位可以用于处理滑动冲突,用于内部拦截法。
当ViewGroup不再拦截事件时,事件会向下分发交由子View处理。首先遍历ViewGroup的所有子元素,然后判断子元素是否可以接收到点击事件。
子元素能否接收到点击事件由两点来衡量:
- 子元素是否在播动画
- 点击事件的坐标是否落入到子元素的区域内
mFirstTouchTarget 真正赋值是在 addTouchTarget 中完成的,mFirstTouchTarget 其实是一种单链表的结构,mFirstTouchTarget是否被赋值将会直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,则ViewGroup将会默认拦截接下来的同一序列中所有的点击事件。
4.3.3 View对点击事件的处理过程
View对点击事件的处理就比较简单了,因为View是一个单独的元素,不会有子元素从而无法向下传递事件,所以只能由他自个处理。
View对点击事件的处理过程:
- 首先会判断是否设置了OnTouchListener,如果OnTouchListener中的onTouch返回true,那么onTouchEvent就不会被调用。
- 接着判断只要View的 CLICKABLE 和 LONG_CLICKABLE 中有一个为true,那么它就会消耗这个事件,onTouchEvent就会返回true。不管是不是VISIBLE。
- 如果当ACTION_UP事件发生时,就会触发performClick方法,如果View设置了OnClickListener,那么performClick方法就会调用onClick。
5. View的滑动冲突
5.1 常见的滑动冲突场景
- 外部滑动方向与内部滑动方向不一致;
- 外部滑动方向与内部滑动方向一致;
- 1与2 两种情况结合;
5.2 滑动冲突处理规则
- 根据滑动方向是水平滑动还是竖直滑动来判断交由谁拦截处理;
- 根据滑动方向与水平方向所形成的夹角进行判断;
- 水平方向与竖直方向 距离差或速度差;
- 根据业务需求来处理判断;
5.3 滑动冲突的解决方案
- 外部拦截法:
- 由ViewGroup的 onInterceptTouchEvent 来进行拦截处理。
- 对 ACTION_DOWN 这个事件不做拦截,因为一旦拦截此事件,那么该序列的剩下事件都将会交由父容器处理。
- 对 ACTION_UP 这个事件也不做拦截,因为若拦截此事件,将可能导致子View无法触发onClick事件。因为 ACTION_UP 会触发 performClick 方法,从而可能会调用 onClick 方法。
- 所以一般情况下,对 ACTION_DOWN 与 ACTION_UP 事件都返回false,ACTION_MOVE 视情况及需求而定是否拦截。
- 内部拦截法:
- 指父容器对任何事件不做拦截,所有事件传递给子View,但是需要配合子View的 requestDisallowInterceptTouchEvent 方法来工作。
- 除了子元素需要做处理外,父元素也要默认拦截除了 ACTION_DOWN 以外的其他事件。为什么不拦截 ACTION_DOWN 事件,因为 FLAG_DISALLOW_INTERCEPT 不会影响 ACTION_DOWN 事件,因为收到 ACTION_DOWN 事件时,标志位将会被重置,所以一旦父容器拦截了该事件,那么所有事件将无法传递到子元素里去。
以上是关于Android——View的事件体系的主要内容,如果未能解决你的问题,请参考以下文章
Android艺术开发探索第三章————View的事件体系(下)