View的事件体系(一)

本章重点
1.view的基础知识
1.1什么是view View是Android中所有控件的基类,View的本身可以是单个空间,也可以是多个控件组成的一组控件,即ViewGroup,ViewGroup继承自View,其内部可以有子View,这样就形成了View树的结构。

1.2 View的位置参数 View的位置主要由它的四个顶点来决定,即它的四个属性:top、left、right、bottom,分别表示View左上角的坐标点(top,left)以及右下角的坐标点(right,bottom)。同时,我们可以得到View的大小:
width = right - left height = bottom - top 而这四个参数可以由以下方式获取: Left =
getLeft(); Right = getRight(); Top = getTop(); Bottom = getBottom();
Android3.0后,View增加了x、y、translationX和translationY这几个参数。其中x和y是
View左上角的坐标,而translationX和translationY是View左上角相对于容器的偏移量。 他们之间的换算关系如下: x = left + translationX; y = top + translationY; 注意:View在平移的过程中,top和left不会改变,改变的是x、y、translationX和translaY。

1.3 MotionEvent和TouchSlop 1 MotionEvent 在手指接触到屏幕后会产生乙烯类的点击事件,如

  • 点击屏幕后离开松开,事件序列为DOWN->UP
  • 点击屏幕滑动一会再松开,事件序列为DOWN->MOVE->…->MOVE->UP 通过MotionEven对象我们可以得到事件发生的x和y坐标,我们可以通过getX/getY和getRawX/getRawY得到,它们的区别是:getX/getY返回的是相对于当前View左上角的x和y坐标,getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
    TouchSloup TouchSloup是系统所能识别出的被认为是滑动的最小距离,这是一个常量,与设备有 关,可通过以下方法获得:
    ViewConfiguration.get(getContext()).getScaledTouchSloup().

1.4 VelocityTracker、GestureDetector和Scroller
1 VelocityTracker 速度追踪,用于追踪手指在滑动过程中的速度,包括水平放向速度和竖直方向速度。 使 用方法:

  1. 在View的onTouchEvent方法中追踪当前单击事件的速度 VelocityRracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event);
  2. 计算速度,获得水平速度和竖直速度 velocityTracker.computeCurrentVelocity(1000); int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity =
    (int)velocityTracker.getYVelocity();
    注意,获取速度之前必须先计算速度,即调用computeCurrentVelocity方法,这里指
    的速度是指一段时间内手指滑过的像素数,1000指的是1000毫秒,得到的是1000毫 秒内滑过的像素数。速度可正可负: 速度 = (终点位置
  • 起点位置) / 时间段
    1. 最后,当不需要使用的时候,需要调用clear()方法重置并回收内存: velocityTracker.clear(); velocityTracker.recycle(); 2 GestureDetector
      手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。 使用方法:
    2. 创建一个GestureDetector对象并实现OnGestureListener接口,根据需要,也可实现 OnDoubleTapListener接口从而监听双击行为: GestureDetector mGestureDetector = new
      GestureDetector(this); //解决长按屏幕后无法拖动的现象
      mGestureDetector.setIsLongpressEnabled(false);
    3. 在目标View的OnTouchEvent方法中添加以下实现: boolean consume = mGestureDetector.onTouchEvent(event); return consume;
    4. 实现OnGestureListener和OnDoubleTapListener接口中的方法,其中常用的方法有: onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和onDoubleTap(双击)。
      建议:如果只是监听滑动相关的,可以自己在onTouchEvent中实现,如果要监听双击这种行为,那么就使GestureDetector。 3
      Scroller 弹性滑动对象,用于实现View的弹性滑动。其本身无法让View他行滑动,需要和View的
      computeScroll方法配合使用才能完成这个功能。 使用方法:

Scroller scroller = new Scroller(mContext); //缓慢移动到指定位置 private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX; //1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX,0,delta,0,1000);
invalidata();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX,mScroller.getCurrY());
postInvalidate();
}
}

2 View的滑动
2.1 使用scrollTo/scrollBy

  1. scrollBy实际调用了scrollTo,它实现了基于当前位置的相对滑动,而scrollTo则实现了绝对滑动。
  2. scrollTo和scrollBy只能改变View的内容位置而不能改变View在布局中的位置。
  3. 滑动偏移量mScrollX和mScrollY的正负与实际滑动方向相反,即从左向右滑动,mScrollX为负值,从上往下滑动mScrollY为负值。
    2.2 使用动画 使用动画移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果使用属性动画,为了能够兼容3.0以下的版本,需要采用开源动画库nineolddandroids。
    如使用属性动画:(View在100ms内向右移动100像素)
    ObjectAnimator.ofFloat(targetView,“translationX”,0,100).setDuration(100).start();
    2.3 改变布局属性 通过改变布局属性来移动View,即改变LayoutParams。
    2.4 各种滑动方式的对比
  4. scrollTo/scrollBy:操作简单,适合对View内容的滑动;
  5. 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
  6. 改变布局参数:操作稍微复杂,适用于有交互的View。
    3 弹性滑动
    3.1 使用Scroller 使用Scroller实现弹性滑动的典型使用方法如下:

Scroller scroller = new Scroller(mContext); //缓慢移动到指定位置 private void smoothScrollTo(int destX,int dextY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
//1000ms内滑向destX,效果就是缓慢滑动 mScroller.startSscroll(scrollX,0,deltaX,0,1000);
invalidate();
} @override
public void computeScroll(){
if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
} }

从上面代码可以知道,我们首先会构造一个Scroller对象,并调用他的startScroll方法,该方法并没有让view实现滑动,只是把参数保存下来,我们来看看startScroll方法的实现就知道了:

public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAminationTimeMills();
mStartX = startX;
mStartY = startY; mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float)mDuration;
}

可以知道,startScroll方法的几个参数的含义,startX和startY表示滑动的起点,dx和dy表示的是滑动的距离,而duration表示的是滑动时间,注意,这里的滑动指的是View内容的滑动,在startScroll方法被调用后,马上调用invalidate方法,这是滑动的开始,invalidate方法会导致View的重绘,在View的draw方法中调用computeScroll方法,computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法进行第二次重绘,一直循环,知道computeScrollOffset()方法返回值为false才结束整个滑动过程。
我们可以看看computeScrollOffset方法是如何获得当前的scrollX和scrollY的:

public boolean computeScrollOffset(){
… int timePassed = (int)(AnimationUtils.currentAnimationTimeMills() - mStartTi me);
if(timePassed < mDuration){
switch(mMode){ case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDuratio nReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(y * mDeltaY);
break;
… } }
return true;
}

到这里我们就基本明白了,computeScroll向Scroller获取当前的scrollX和scrollY其实是通过计算时间流逝的百分比来获得的,每一次重绘距滑动起始时间会有一个时间间距,通过这个时间间距Scroller就可以得到View当前的滑动位置,然后就可以通过scrollTo方法来完成View的滑动了。

3.2 通过动画 动画本身就是一种渐近的过程,因此通过动画来实现的滑动本身就具有弹性。实现也很简单: ObjectAnimator.ofFloat(targetView,“translationX”,0,100).setDuration(100).start();
当然,我们也可以利用动画来模仿Scroller实现View弹性滑动的过程:

final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000); animator.addUpdateListener(new AnimatorUpdateListener(){ @override
public void onAnimationUpdate(ValueAnimator animator){
float fraction= animator.getAnimatedFraction(); mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0);
}
});
animator.start();

上面的动画本质上是没有作用于任何对象上的,他只是在1000ms内完成了整个动画过程,利用这个特性,我们就可以在动画的每一帧到来时获取动画完成的比例,根据比例计算出View所滑动的距离。

3.3 使用延时策略 延时策略的核心思想是通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Hander和View的postDelayed方法,也可以使用线程的sleep方法。
下面以Handler为例:

private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint(“HandlerLeak”)
private Handler handler = new handler(){ public void handleMessage(Message msg){
switch(msg.what){
case MESSAGE_SCROLL_TO:
mCount ++ ;
if (mCount <= FRAME_COUNT){
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0); mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
}
break;
default :
break;
} } }

1. View基础知识

1.1 View的位置参数

View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:top、left、
right、bottom,其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,
bottom是右下角纵坐标。需要注意的是,这些坐标都是相对于View的父容器来说的,因此它是一种相对坐标,View的坐标和父容器的关系如图在这里插入图片描述所示。
如图所示,View的宽高和坐标的关系:
width = right - left
height = bottom - top
那么如何得到View的这四个参数呢?也很简单,在View的源码中它们对应于mLeft、
mRight、mTop和mBottom这四个成员变量,获取方式如下所示。
Left=getLeft();
Right=getRight();
Top=getTop;
Bottom=getBottom()。
从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这四个参数。

1.2 MotionEvent和TouchSlop

1. MotionEvent
在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:

  • ACTION_DOWN——手指刚接触屏幕;
  • ACTION_MOVE——手指在屏幕上移动;
  • ACTION_UP——手机从屏幕上松开的一瞬间。

正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,考虑如下几种情况:

  • 点击屏幕后离开松开,事件序列为DOWN -> UP;
  • 点击屏幕滑动一会再松开,事件序列为DOWN -> MOVE -> … > MOVE -> UP。
    上述三种情况是典型的事件序列,同时通过MotionEvent对象我们可以得到点击事件
    发生的x和y坐标。为此,系统提供了两组方法:getX/getY和getRawX/getRawY。它们的区别其实很简单,getX/getY返回的是相对于当前View左上角的x和y坐标,而
    getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。

2. TouchSlop
TouchSlop是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕
上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操
作。原因很简单:滑动的距离太短,系统不认为它是滑动。这是一个常量,和设备有关,
在不同设备上这个值可能是不同的,通过如下方式即可获取这个常量:
ViewConfiguration. get(getContext()).getScaledTouchSlop()。这个常量有什么意义呢?当我
们在处理滑动时,可以利用这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于
这个值,我们就可以认为未达到滑动距离的临界值,因此就可以认为它们不是滑动,这样
做可以有更好的用户体验。其实如果细心的话,可以在源码中找到这个常量的定义,在
frameworks/base/core/res/res/values/config.xml文件中,如下所示。这
个“config_viewConfigurationTouchSlop”对应的就是这个常量的定义。

<!--Base "touch slop" value used by ViewConfiguration as a movement threshold
where scrolling should begin. -->
<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>

1.3 VelocityTracker、GestureDetector和Scroller

1. VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。它的使
用过程很简单,首先,在View的onTouchEvent方法中追踪当前单击事件的速度:

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

接着,当我们先知道当前的滑动速度时,这个时候可以采用如下方式来获得当前的速
度:

velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

在这一步中有两点需要注意,第一点,获取速度之前必须先计算速度,即
getXVelocity和getYVelocity这两个方法的前面必须要调用computeCurrentVelocity方法;第二点,这里的速度是指一段时间内手指所滑过的像素数,比如将时间间隔设为1000ms
时,在1s内,手指在水平方向从左向右滑过100像素,那么水平速度就是100。注意速度可以为负数,当手指从右往左滑动时,水平方向速度即为负值,这个需要理解一下。速度的计算可以用如下公式来表示:
速度=(终点位置-起点位置)/时间段
根据上面的公式再加上Android系统的坐标系,可以知道,手指逆着坐标系的正方向
滑动,所产生的速度就为负值。另外,computeCurrentVelocity这个方法的参数表示的是一个时间单元或者说时间间隔,它的单位是毫秒(ms),计算速度时得到的速度就是在这
个时间间隔内手指在水平或竖直方向上所滑动的像素数。针对上面的例子,如果我们通过
velocityTracker.computeCurrentVelocity(100)来获取速度,那么得到的速度就是手指在
100ms内所滑过的像素数,因此水平速度就成了10像素/每100ms(这里假设滑动过程是匀速的),即水平速度为10,这点需要好好理解一下。
最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存:

velocityTracker.clear();
velocityTracker.recycle();

上面就是如何使用VelocityTracker对象的全过程,看起来并不复杂。
2. GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。要使用
GestureDetector也不复杂,参考如下过程。
首先,需要创建一个GestureDetector对象并实现OnGestureListener接口,根据需要我
们还可以实现OnDoubleTapListener从而能够监听双击行为:

GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);

接着,接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添
加如下实现:

boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

做完了上面两步,我们就可以有选择地实现OnGestureListener和OnDoubleTapListener
中的方法了,这两个接口中的方法介绍如下图所示。
OnGestureListener和OnDoubleTapListener中的方法介绍

如果只是监听滑动相关的,建议自己在onTouchEvent中实现,如果要监听双击这种行为的话,那么就使用GestureDetector。
3. Scroller
弹性滑动对象,用于实现View的弹性滑动。我们知道,当使用View的
scrollTo/scrollBy方法来进行滑动时,其过程是瞬间完成的,这个没有过渡效果的滑动用户
体验不好。这个时候就可以使用Scroller来实现有过渡效果的滑动,其过程不是瞬间完成
的,而是在一定的时间间隔内完成的。Scroller本身无法让View弹性滑动,它需要和View
的computeScroll方法配合使用才能共同完成这个功能。那么如何使用Scroller呢?它的典型代码是固定的,如下所示。

Scroller scroller = new Scroller(mContext);
// 缓慢滚动到指定位置
private void smoothScrollTo(int destX,int destY) {
int scrollX = getScrollX();
int delta = destX -scrollX;
// 1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX,0,delta,0,1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}

2.View的滑动

通过三种方式可以实现View的滑动:第一种是通过View本身提供的scrollTo/scrollBy
方法来实现滑动;第二种是通过动画给View施加平移效果来实现滑动;第三种是通过改
变View的LayoutParams使得View重新布局从而实现滑动。从目前来看,常见的滑动方式
就这么三种,下面一一进行分析。

2.1 使用scrollTo/scrollBy

为了实现View的滑动,View提供了专门的方法来实现这个功能,那就是scrollTo和
scrollBy,我们先来看看这两个方法的实现,如下所示。

/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int,int,int,int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x,int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX,mScrollY,oldX,oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int,int,int,int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x,int y) {
scrollTo(mScrollX + x,mScrollY + y);
}

从上面的源码可以看出,scrollBy实际上也是调用了scrollTo方法,它实现了基于当前
位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。利用
scrollTo和scrollBy来实现View的滑动,这不是一件困难的事,但是我们要明白滑动过程中
View内部的两个属性mScrollX和mScrollY的改变规则,这两个属性可以通过getScrollX和
getScrollY方法分别得到。
这里先简要概况一下:在滑动过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离。View边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中的内容的边缘,scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。mScrollX和mScrollY的单位为像素,并且当View左边缘在View内容左边缘的右边时,mScrollX为正值,反之为负值;当View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负值。换句话说,如果从左向右滑动,那么mScrollX为负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。
为了更好地理解这个问题,下面举个例子,如下图所示。在图中假设水平和竖直方
向的滑动距离都为100像素,针对图中各种滑动情况,都给出了对应的mScrollX和
mScrollY的值。根据上面的分析,可以知道,使用scrollTo和scrollBy来实现View的滑动,
只能将View的内容进行移动,并不能将View本身进行移动,也就是说,不管怎么滑动,
也不可能将当前View滑动到附近View所在的区域,这个需要仔细体会一下。

在这里插入图片描述

2.2 使用动画

上一节介绍了采用scrollTo/scrollBy来实现View的滑动,本节介绍另外一种滑动方
式,即使用动画,通过动画我们能够让一个View进行平移,而平移就是一种滑动。使用
动画来移动View,主要是操作View的translationX和translationY属性,既可以采用传统的
View动画,也可以采用属性动画,如果采用属性动画的话,为了能够兼容3.0以下的版
本,需要采用开源动画库nineoldandroids(http://nineoldandroids.com/)。
采用View动画的代码,如下所示。此动画可以在100ms内将一个View从原始位置向右
下角移动100个像素。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal" >
<translate
android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="100"
android:toYDelta="100" />
</set>

如果采用属性动画的话,就更简单了,以下代码可以将一个View在100ms内从原始位
置向右平移100像素。

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

2.3 改变布局参数

改变布局参数,即改变LayoutParams。
我们想把一个Button向右平移100px,我们只需要
将这个Button的LayoutParams里的marginLeft参数的值增加100px即可,是不是很简单呢?还有一种情形,为了达到移动Button的目的,我们可以在Button的左边放置一个空的
View,这个空View的默认宽度为0,当我们需要向右移动Button时,只需要重新设置空
View的宽度即可,当空View的宽度增大时(假设Button的父容器是水平方向的
LinearLayout),Button就自动被挤向右边,即实现了向右平移的效果。如何重新设置一
个View的LayoutParams呢?很简单,如下所示。

MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton1.requestLayout();
//或者mButton1.setLayoutParams(params);

通过改变LayoutParams的方式去实现View的滑动同样是一种很灵活的方法,需要根据
不同情况去做不同的处理。

2.4 各种滑动方式的对比

上面分别介绍了三种不同的滑动方式,它们都能实现View的滑动,那么它们之间的
差别是什么呢?
先看scrollTo/scrollBy这种方式,它是View提供的原生方法,其作用是专门用于View
的滑动,它可以比较方便地实现滑动效果并且不影响内部元素的单击事件。但是它的缺点
也是很显然的:它只能滑动View的内容,并不能滑动View本身。
再看动画,通过动画来实现View的滑动,这要分情况。如果是Android 3.0以上并采用
属性动画,那么采用这种方式没有明显的缺点;如果是使用View动画或者在Android 3.0以
下使用属性动画,均不能改变View本身的属性。在实际使用中,如果动画元素不需要响
应用户的交互,那么使用动画来做滑动是比较合适的,否则就不太适合。但是动画有一个
很明显的优点,那就是一些复杂的效果必须要通过动画才能实现。
最后再看一下改变布局这种方式,它除了使用起来麻烦点以外,也没有明显的缺点,
它的主要适用对象是一些具有交互性的View,因为这些View需要和用户交互,直接通过
动画去实现会有问题,这在3.2.2节中已经有所介绍,所以这个时候我们可以使用直接改变
布局参数的方式去实现。
针对上面的分析做一下总结,如下所示。

  • scrollTo/scrollBy:操作简单,适合对View内容的滑动;
  • 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
  • 改变布局参数:操作稍微复杂,适用于有交互的View。

3.弹性滑动

3.1 使用Scroller

下面我们来分析一下它的源码,从而探究为什么它能实现View的弹性滑动。

Scroller scroller = 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();
}
}

上面是Scroller的典型的使用方法,这里先描述它的工作原理:当我们构造一个Scroller对象并且调用它的startScroll方法时,Scroller内部其实什么也没做,它只是保存了我们传递的几个参数,这几个参数从startScroll的原型上就可以看出来,如下所示。

public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}

这个方法的参数含义很清楚,startX和startY表示的是滑动的起点,dx和dy表示的是要滑动的距离,而duration表示的是滑动时间,即整个滑动过程完成所需要的时间,注意这里的滑动是指View内容的滑动而非View本身位置的改变。可以看到,仅仅调用startScroll方法是无法让View滑动的,因为它内部并没有做滑动相关的事,那么Scroller到底是如何让View弹性滑动的呢?答案就是startScroll方法下面的invalidate方法,虽然有点不可思议,但是的确是这样的。invalidate方法会导致View重绘,在View的draw方法中又会去调用computeScroll方法,computeScroll方法在View中是一个空实现,因此需要我们自己去实现,上面的代码已经实现了computeScroll方法。正是因为这个computeScroll方法,View才能实现弹性滑动。这看起来还是很抽象,其实这样的:当View重绘后会在draw方法中调用computeScroll,而computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致computeScroll方法被调用;然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo方法滑动到新的位置,如此反复,直到整个滑动过程结束。
我们再看一下Scroller的computeScrollOffset方法的实现,如下所示。

/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
...
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() -
mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed *
mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
...
}
}
return true;
}

这个方法会根据时间的流逝来计算出当前的scrollX和scrollY的值。计算方法也很简单,大意就是根据时间流逝的百分比来算出scrollX和scrollY改变的百分比并计算出当前的值,这个过程类似于动画中的插值器的概念,这里我们先不去深究这个具体过程。这个方法的返回值也很重要,它返回true表示滑动还未结束,false则表示滑动已经结束,因此当这个方法返回true时,我们要继续进行View的滑动。
通过上面的分析,我们应该明白Scroller的工作原理了,这里做一下概括:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是Scroller的工作机制。由此可见,Scroller的设计思想是多么值得称赞,整个过程中它对View没有丝毫的引用,甚至在它内部连计时器都没有。

3.2 通过动画

动画本身就是一种渐近的过程,因此通过它来实现的滑动天然就具有弹性效果,比如以下代码可以让一个View的内容在100ms内向左移动100像素。

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

不过这里想说的并不是这个问题,我们可以利用动画的特性来实现一些动画不能实现的效果。还拿scrollTo来说,我们也想模仿Scroller来实现View的弹性滑动,那么利用动画的特性,我们可以采用如下方式来实现:

final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
float fraction = animator.getAnimatedFraction();
mButton1.scrollTo(startX + (int) (deltaX * fraction),0);
}
});
animator.start();

在上述代码中,我们的动画本质上没有作用于任何对象上,它只是在1000ms内完成了整个动画过程。利用这个特性,我们就可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离。注意,这里的滑动针对的是View的内容而非View本身。可以发现,这个方法的思想其实和Scroller比较类似,都是通过改变一个百分比配合scrollTo方法来完成View的滑动。需要说明一点,采用这种方法除了能够完成弹性滑动以外,还可以实现其他动画效果,我们完全可以在onAnimationUpdate方法中加上我们想要的其他操作。

3.3 使用延时策略

本节介绍另外一种实现弹性滑动的方法,那就是延时策略。它的核心思想是通过发送一系列延时消息从而达到一种渐近式的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果。对于sleep方法来说,通过在while循环中不断地滑动View和sleep,就可以实现弹性滑动的效果。下面采用Handler来做个示例,其他方法请读者自行去尝试,思想都是类似的。下面的代码在大约1000ms内将View的内容向左移动了100像素,代码比较简单,就不再详细介绍了。之所以说大约1000ms,是因为采用这种方式无法精确地定时,原因是系统的消息调度也是需要时间的,并且所需时间不定。

private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int mCount = 0;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SCROLL_TO: {
mCount++;
if (mCount <= FRAME_COUNT) {
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,
DELAYED_TIME);
}
break;
}
default:
break;
}
};
};

上面几种弹性滑动的实现方法,在介绍中侧重更多的是实现思想,在实际使用中可以对其灵活地进行扩展从而实现更多复杂的效果。

DEMO下载地址:

滑动方式Demo下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值