一、View基础知识
什么是View
View是一种界面层的控件的一种抽象,它代表了一个控件。ViewGroup也继承了View,这就意味着View本身就可以是单个控件也可以是由多个控件组成的一组控件,通过这种关系就形成了View树的结构。
View的位置参数
View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:top、left、right、bottom,这些坐标都是相对于View的父容器来说的,因此它是一种相对坐标。
- top是左上角纵坐标,Left=getLeft()
- left是左上角横坐标,Right=getRight()
- right是右下角横坐标,Top=getTop
- bottom是右下角纵坐标,Bottom=getBottom()
得出View的宽高和坐标的关系
width = right - left
height = bottom - top
从Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY,其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且translationX和translationY的默认值是0,和View的四个基本的位置参数一样,View也为它们提供了get/set方法,这几个参数的换算关系如下所示。
x=left+translationX
y=top+translationY
需要注意的是,View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数。
常用类
MotionEvent
在手指接触屏幕后所产生的一系列事件中,典型的事件类型有:ACTION_DOWN、ACTION_MOVE、ACTION_UP
通过MotionEvent对象我们可以得到点击事件发生的x和y坐标。为此,系统提供了两组方法:getX/getY和getRawX/getRawY。它们的区别其实很简单,getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
TouchSlop
TouchSlop是系统所能识别出的被认为是滑动的最小距离。
这是一个常量,和设备有关,在不同设备上这个值可能是不同的,通过如下方式即可获取这个常量:ViewConfiguration. get(getContext()).getScaledTouchSlop(),源码中是8dp。
VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
- 首先,在View的onTouchEvent方法中追踪当前单击事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
- 接着,当我们先知道当前的滑动速度时,这个时候可以采用如下方式来获得当前的速度:
velocityTracker.computeCurrentVelocity(1000); //速度的单位,这里是1s
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
注意速度可以为负数,当手指从右往左滑动时,水平方向速度即为负值
速度=(终点位置-起点位置)/时间段
- 最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
比较常用的有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和onDoubleTap(双击)。
实际开发中,可以不使用GestureDetector,完全可以自己在View的onTouchEvent方法中实现所需的监听,这里有一个建议供读者参考:如果只是监听滑动相关的,建议自己在onTouchEvent中实现,如果要监听双击这种行为的话,那么就使用GestureDetector。
Scroller
弹性滑动对象,用于实现View的弹性滑动。
Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。
二、View的事件分发机制
点击事件的传递规则
点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent
- dispatchTouchEvent,用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
- onInterceptTouchEvent,在上述方法内部调用,用来判断是否拦截某个事件,如果当前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;
}
点击事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。由此可见,给View设置的OnTouchListener,其优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的OnClickListener,其优先级最低,即处于事件传递的尾端。
点击事件传递过程的顺序:
Activity -> Window -> ViewGroup -> View
dispatchTouchEvent -> onInterceptTouchEvent -> OnTouchListener -> onTouchEvent -> OnClickListener
事件传递说明
- ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouch-Event方法默认返回false。
- View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable 和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
- onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
三、View的滑动
常见的三种滑动方式
-
第一种使用scrollTo/scrollBy
通过View本身提供的scrollTo/scrollBy方法来实现滑动;
scrollBy实际上也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。
mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离。
scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。mScrollX和mScrollY的单位为像素。
如果从左向右滑动,那么mScrollX为负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。
scrollTo/scrollBy操作简单,适合对View内容的滑动。
-
第二种使用动画
通过动画给View施加平移效果来实现滑动;
View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括宽/高,会有个问题单击新位置不会触发onClick事件,因为在新位置上只是View的影像而已。
使用属性动画可以解决上面的问题。
使用动画操作简单,主要适用于没有交互的View和实现复杂的动画效果
-
第三种改变布局参数
通过改变View的LayoutParams使得View重新布局从而实现滑动。
改变布局参数操作稍微复杂,适用于有交互的View。
弹性滑动
弹性滑动不会生硬地滑动过去,用户体验更好。实现方式有很多,但是它们都有一个共同思想:将一次大的滑动分成若干次小的滑动并在一个时间段内完成。有三种实现方式:
-
使用Scroller
Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是Scroller的工作机制。Scroller的设计非常赞。
-
通过动画
动画本身就是一种渐近的过程,因此通过它来实现的滑动天然就具有弹性效果。
-
使用延时策略
可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法来实现。
View的滑动冲突
常见的滑动冲突场景
- 场景1——外部滑动方向和内部滑动方向不一致
将ViewPager和Fragment配合使用所组成的页面滑动效果,本来这种情况下是有滑动冲突的,但是ViewPager内部处理了这种滑动冲突。
如果我们采用的不是ViewPager而是ScrollView等,那就必须手动处理滑动冲突了。处理规则是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件。是否是左右滑动的判断条件很多,比如可以依据滑动路径和水平方向所形成的夹角,也可以依据水平方向和竖直方向上的距离差来判断,某些特殊时候还可以依据水平和竖直方向的速度差来做判断。
- 场景2——外部滑动方向和内部滑动方向一致
这种场景主要是指内外两层同时能上下滑动或者内外两层同时能左右滑动。判断是内部滑动还是外部滑动,根据业务上的需求可以找出相应的处理规则。
- 场景3——上面两种情况的嵌套
和场景2一样,也是只能从业务上找到突破点,找到处理内外滑动的规则。
滑动冲突的解决方式
- 外部拦截法
所谓外部拦截法是指点击事情都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。
外部拦截法需要重写父容器的onInterceptTouchEvent方法,根据内外滑动的规则,在内部做相应的拦截即可。
- 内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。
使用起来较外部拦截法稍显复杂,子控件需要重新dispatchTouchEvent方法,用到requestDisallowInterceptTouchEvent方法,父控件要重写onInterceptTouchEvent方法。
参考书籍《Android开发艺术探索》