View的基础知识主要有:View的位置参数、MotionEvent、TouchSlop对象、VelocityTracker、GestureDelector和Scroller对象等等。
View是Android中所有控件的基类,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。
手指接触屏幕产生的一系列事件,典型的事件如下几种:
同时通过MotionEvent对象我们可以获取到点击事件发生的 x 和 y 坐标。系统提供了两组方法:
TouchSlop 是系统所能识别出的被认为是滑动的最小距离,如果两次滑动之间的距离小于这个常量,那么系统就不认为是在进行滑动操作。这是一个常量,与设别有关,不同设备该常量可能不一样。
可以通过代码获取这个TouchSlop常量值:ViewConfiguration.get(context).getScaledTouchSlop()
应用场景:当需要处理滑动时,可以利用这个常量来做一些过滤,例如两次滑动的距离小于这个值,我们就可以认为未达到滑动的临界值,因此可以认为它们不是滑动。
速度追踪,用于追踪手指在滑动过程中的速度。包括 水平方向的速度 和 垂直方向的速度。
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();
手势检测,用于辅助检测用户的 单击、滑动、长按、双击 等行为。
在GestureDelector使用过程中,首先需要创建一个GestureDelector对象并实现 OnGestureListener 接口,根据需要我们实现内部对应的接口。接着我们需要接管目标View的 onTouchEvent 方法,在待监听的View的 onTouchEvent 方法中实现:
boolean consume = mGestureDelector.onTouchEvent(event);
returen consume;
在日常开发中,比较常用的有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和 onDoubleTap(双击)。
建议:如果只是监听滑动相关的,建议自己在onTouchEvent中实现,如果要监听双击等这种行为的,那么使用GestureDelector。
弹性滑动对象,用于实现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();
}
}
View的滑动可以通过三种方式来实现:
scrollBy 实际上也调用了 scrollTo,实现了基于当前位置的相对滑动,而 scrollTo 实现了基于传递参数的绝对滑动。
在滑动的过程中,View内部有两个属性 mScrollX 和 mScrollY,mScrollX 始终等于View的左边缘与View内容左边缘的水平方向的距离,mScrollY 总是等于View的上边缘与View的内容上边缘的竖直方向的距离。
scrollTo 和 scrollBy 只能改变 View内容的位置 而不能改变 View的位置。
主要操作的是View的 translationX 和 translationY 属性。可以采用传统的动画,也可以采用属性动画。
需要注意的一点是,View动画只是对View的影像做操作,并不能改变View的位置参数,包括宽高。若希望动画结束后可以保留当前的状态,则必须将fillAfter属性设置为true,否则动画结束后结果会消失。
注意:使用动画来实现View动画并不能改变View的位置。使用属性动画可以解决该问题。
改变布局参数,即改变LayoutParams。
思路:将一次大的滑动分成几次小的滑动并在一个时间段内完成。
实现弹性滑动的方式主流的做法如下:
通过Scroller的滑动指View的内容滑动而不是View本身位置的参数。
Scroller本身是无法实现滑动效果的,需要结合View的 computeScroll 方法来配合完成。通过不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔。
View的每一次重绘都会导致View进行小幅度的滑动,而多个小幅度的滑动就组成了弹性滑动,这就是Scroller的工作原理。
思想其实与Scroller类似,都是通过改变一个百分比配合scrollTo来完成View的重绘。
通过 Handler#postDelayed 或者 Thread#sleep 来完成延时策略。
注意:采用Handler来完成延时时,所设定的时间是无法精准地定时,因为系统的消息调度也是需要时间的。
通过View的事件分发机制,可以解决View的一大难题——View的滑动冲突。
点击事件的传递规则,要分析的对象就是 MotionEvent,即点击事件。
点击事件在分发的过程中,有三个很重要的方法:dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
事件传递优先级:OnTouchListener > onTouchEvent > OnClickListener。
当一个点击事件产生后,传递过程是:Activity -> Window -> View。
当一个点击事件发生时,最先传递给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。
ViewGroup在如下两种情况下会判断是否拦截当前事件:
FLAG_DISALLOW_INTERCEPT,该标志位是通过 requestDisallowInterceptTouchEvent 来设置的,一般用于子View中。一旦该标志位被设置,那么ViewGroup无法拦截除了ACTION_DOWN以外的点击事件。为什么是ACTION_DOWN以外的事件?因为ACTION_DOWN会重置FLAG_DISALLOW_INTERCEPT,将导致子View中设置这个标志位无效。
注意:
当ViewGroup不再拦截事件时,事件会向下分发交由子View处理。首先遍历ViewGroup的所有子元素,然后判断子元素是否可以接收到点击事件。
子元素能否接收到点击事件由两点来衡量:
mFirstTouchTarget 真正赋值是在 addTouchTarget 中完成的,mFirstTouchTarget 其实是一种单链表的结构,mFirstTouchTarget是否被赋值将会直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,则ViewGroup将会默认拦截接下来的同一序列中所有的点击事件。
View对点击事件的处理就比较简单了,因为View是一个单独的元素,不会有子元素从而无法向下传递事件,所以只能由他自个处理。
View对点击事件的处理过程: