前言
这篇文章可以作为基础看看,但是有时候基础就是细节,不一定所有人都记得,所以基础也要记录一下。都熟悉的话也可以看看其他系列文章:
View事件分发机制(源码分析篇)
Android一步一步追踪View的工作原理
View是Android中所有控件的基类,是一种界面层的控件的一种抽象,它代表了一个控件。除了View,还有ViewGroup,其内部可以包含多个控件,即一组View。在Android的设计中,ViewGroup也继承了View,这就意味着View本身就可以是单个控件也可以是由多个控件组成的一组控件,通过这种关系形成了View树的结构。
View的位置参数
View的位置参数主要由它的四个顶点来决定,分别对应View的四个属性:top、left、right、bottom,其中top是左上角的纵坐标,left是左上角的横坐标,right是右下角的横坐标,bottom是右下角的纵坐标。需要注意的是,这些坐标都是相对View的父容器来说的,因此它是一种相对坐标。另外在Android中,x轴和y轴的正方向分别是右和下。
根据View的位置参数可以得到View的宽高和坐标的关系如下:
width = right - left;
height = bottom - top;
在View的源码中View的四个位置参数对应了mLeft、mRight、mTop和mBottom这四个成员变量,View中提供了对应的getXX()方法来获取对应的位置参数。
在Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY,其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且translationX和translationY的默认值为0,View同样提供了get/set方法。
几个参数之间的关系如下:
x = left + translationX;
y = top + translationY;
需要注意的是,View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数。
MotionEvent和TouchSlop
MotionEvent
在手指接触屏幕后所产生的一系列事件中,典型的事件有如下几种:
- ACTION_DOWN——手指刚接触屏幕;
- ACTION_MOVE——手指在屏幕上滑动;
- ACTION_UP——手指从屏幕上松开的瞬间。
正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,例如点击屏幕滑动一会再松开,事件序列为DWON->MOVE->…->MOVE->UP。
上述的三种情况是典型的事件序列,同时通过MotionEvent对象可以得到点击事件发生的x和y坐标。系统提供了两组方法:getX/getY和getRawX/getRawY。getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
TouchSlop
TouchSlop是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕上滑动时,当两次滑动之间的距离小于该值,则认为不是在进行滑动。这是一个常量,和设备有关,在不同设备上这个值可能是不同的,通过以下方式获取得到这个常量:
ViewConfiguration.get(context).getScaledTouchSlop()
通常可以利用该值对用户的滑动进行过滤。
VelocityTracker、GestureDetector和Scroller
VelocityTracker
VelocityTracker用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。使用的一个例子如下所示,在View的onTouchEvent方法中使用速度追踪。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(event);
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
// 速度追踪
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
Toast.makeText(mContext, "移动速度X方向:" + xVelocity +
"\nY方向:" + yVelocity, Toast.LENGTH_SHORT).show();
break;
}
return true;
}
需要注意的两点:
第一,获取速度之前必须先计算速度,即getXVelocity和getYVelocity这两个方法的前面必须调用computeCurrentVelocity方法;
第二,这里的速度是指一段时间内手指所滑过的像素数。注意,速度可以为负数,当手指从右向左滑动时,水平方向速度即为负值。
这里的终点位置是根据addMovement中添加进入的event来进行判断的,每个event都有一个对应的坐标,当调用computeCurrentVelocity方法进行计算速度时将在添加进去的event中获取最后一个evnet的坐标作为终点位置。
另外,当不需要使用速度跟踪器时,需要调用clear方法来重置并回收内存。
// 回收
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。手势检测的使用并不复杂,首先创建一个GestureDetetor对象并实现OnGestureListener接口,根据需要可以实现OnDoubleTapListener接口从而监听双击行为。
// 创建并传入OnGestureListener接口的实现
GestureDetector gestureDetector = new GestureDetector(context, this);
// 解决长按屏幕后无法拖动的现象
gestureDetector.setIsLongpressEnabled(false);
// 设置双击行为监听接口
gestureDetector.setOnDoubleTapListener(this);
接着接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中,添加如下实现。
boolean consume = gestureDetector.onTouchEvent(event);
return consume;
事实上就是将View的事件处理从onTouchEvent委托到GestureDetector中,将待监听的View的事件处理委托到GestureDetector中进行处理。OnGestureListener接口和OnDoubleTabListener接口中的方法介绍如下所示。
方法名 | 描述 | 所属接口 |
---|---|---|
onDown | 手指轻轻触摸屏幕的一瞬间,由1个ACTION_DOWN触发 | onGestureListener |
onShowPress | 手指轻轻触摸屏幕,尚未松开或拖动,由1个ACTION_DOWN触发 | onGestureListener |
onSingleTapUp | 手指(轻轻触摸屏幕)松开,伴随着1个ACTION_UP而触发,这是单击行为 | onGestureListener |
onScroll | 手指按下屏幕并拖动,由1个ACTION_DOWN,多个ACTION_MOVE触发,这是拖动行为 | onGestureListener |
onLongPress | 用户长久按着屏幕不放,即长按 | onGestureListener |
onFling | 用户按下触摸屏、快速滑动后松开,由1个ACTION_DOWN,多个ACTION_MOVE和1个ACTION_UP触发,这是快速滑动行为 | onGestureListener |
onDoubleTap | 双击,由2次连续的单击组成,它不可能和onSingleTapConfirmed共存 | onDoubleTabListener |
onSingleTabConfirmed | 严格的单击行为。如果触发了该行为,后面紧跟的另一个单击行为只可能是单击,而不可能是双击中的一次单击。 | onDoubleTabListener |
onDoubleTabEvent | 表示发生了双击行为,在双击期间,ACTION_DOWN、ACTION_MOVE和ACTION_UP都会触发此回调。 | onDoubleTabListener |
在实际开发中,可以不使用GestureDetector,完全可以在View的onTouchEvent方法中实现所需的监听。建议可以这样:如果只是监听滑动相关的,建议在onTouchEvent中实现,如果要监听双击行为,那么就使用GestureDetector。
View的滑动
使用scrollTo/scrollBy
为了实现View的滑动,View提供了专门的方法来实现这个功能,那就是scrollTo和scrollBy。根据源码,scrollBy实际上也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。
在滑动过程中View内部有两个属性mScrollX和mScrollY其改变规则:在滑动过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总是等于View上边缘和View内容上边缘在垂直方向上的距离。
注意的一点是,mScrollX和mScrollY的单位是像素,并且当View左边缘在View内容左边缘右边时,mScrollX为正值;当View上边缘在View内容下边时,mScrollY为正值。
需要经过上述的描述,可以发现使用scrollTo和scrollBy来实现View的滑动,只能将View的内容进行移动,而不能将View本身进行移动。并且,scrollTo和scrollBy的滑动过程是瞬间完成的。
使用动画
使用动画来移动View,主要是操作View的translationX和translationY属性,既可以采用传统的补间动画,也可以采用属性动画,如果采用属性动画的话,为了能够兼容3.0以下版本,需要采用开源动画库nineoldandroids。如下为使用属性动画实现View滑动的方式。
// 使用属性动画实现View的滑动——平移
ObjectAnimator.ofFloat(view, "translationX", 0f, 100f)
.setDuration(100)
.start();
改变布局参数
View的第三种实现滑动的方式是改变布局参数,即改变LayoutParams。通过改变LayoutParams的方式去实现View的滑动是一种很灵活的方法,需要根据不同的情况去做不同的处理。实现方式为设置LayoutParams里的marginLeft和marginTop参数。
// 通过改变布局参数使得View滑动
ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) view.getLayoutParams();
layoutParams.leftMargin = x + (rawX - startX);
layoutParams.topMargin = y + (rawY - startY);
view.requestLayout(); // 或者 view.setLayoutParams(layoutParams);
各种滑动方式的对比
scrollTo/scrollBy方式:View提供的原生方法,操作简单并且不影响内部元素的单击事件,但是只能滑动View的内容,不能滑动View本身。适合对View内容的滑动。
动画:采用属性动画来滑动没有任何缺点,但是使用补间动画时其无法改变View本身的属性。操作简单,主要适用于没有交互的Veiw和实现复杂的动画效果。
改变布局方式:操作稍微复杂,适用于有交互的View。
View的弹性滑动
对于非弹性滑动,其无法实现渐近式滑动(弹性滑动),用户体验极差,比如scrollTo/scrollBy以及改变布局参数的方法,由于其瞬时性,因此属于非弹性滑动。实现弹性滑动的方式有很多,但是其共同的思想是:将一次大的滑动分成若干次小的滑动并在一个时间段内完成。弹性滑动的实现方式有:Scroller,动画以及延迟策略等。
使用Scroller
Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能,使用方式如下。
Scroller mScroller = new Scroller(mContext);
// 缓慢滚动到指定位置
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int deltaX = destX - scrollX;
// 1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollx, 0, deltaX, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
调用startScroll方法后,Scroller内部其实什么都没有做,它只是保存了传递的参数。需要注意的是这里的滑动指的是View内容的滑动而非View本身位置的改变。使得View弹性滑动的是invalidate方法,该方法会导致View的重绘,在View中draw方法又会去调用computeScroll方法,而该方法通过调用Scroller的方法进行滑动,在通过调用postInvalidate方法进行二次重绘,以此类推。
而computeScrollOffset会根据时间的流逝来计算当前的scrollX和scrollY的值。
总结一下Scroller的工作原理:Scroller配合View的computerScroll方法完成弹性滑动效果,它不断地让View重绘,而每次重绘距滑动起始时间会有一个时间间隔,通过这个间隔Scroller就可以得出View当前的滑动位置,并通过scrollTo进滑动。
使用动画
动画本身就是一种渐近的过程,因此通过它来实现的滑动天然就具有弹性效果。
在上一个话题中已经介绍了动画的使用方式,相关文章也有很多,不再多做介绍。
使用延时策略
延时策略的核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。使用Handler实现的一种延时策略如下所示。
private static final int MESSAGE_SCROLL_TO = 0;
private int mCount = 0;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch(msg.what) {
case MSSAGE_SCROLL_TO:
mCount ++;
if (mCount <= 30) {
float fraction = mCount / 30f;
int scrollX = (int) (fraction * 100);
mImageView.scrollTo(scrollX, 0);
mHandler.sentEmptyMessageDelayed(MESSAGE_SCROLL_TO, 33);
}
break;
}
}
}