实现View的弹性滑动

所谓弹性滑动,也就是渐进式滑动,核心思想就是将一次大的滑动,划分成若干的小的滑动,并在一个时间段内完成,实现View的弹性滑动的三种方式:

  • 采用延时策略
  • 采用动画方式
  • 采用Scroller

采用延时策略

延时策略的核心思想是通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Hander和View的postDelayed方法,也可以使用线程的sleep方法。使用Hander和View的postDelayed方法可以参考下面的代码,对于sleep方法来说,通过在while方法中不断的滑动View和Sleep,就可以实现弹性滑动效果。
下面以Handler为例,下面的代码在1000ms内,将View向左移动了100像素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
}
}

sleep方法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

private int x=0;
private int y=0;

mButton = (Button) findViewById(R.id.bt_test);

final Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
mButton.scrollTo(x++,y++);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});

mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thread.start();
}
});

采用动画方式

动画本身就是一种渐近的过程,因此通过动画来实现的滑动本身就具有弹性。实现也很简单,以下代码让一个view在100ms内,移动了100个像素:

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

利用动画来模仿Scroller实现View弹性滑动的过程:

1
2
3
4
5
6
7
8
9
10
11
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所滑动的距离,原理和Scroller的原理类似。

采用Scroller

用法

分为三步:
1.创建一个Scroller对象,一般在View的构造器中创建:

1
2
3
4
5
6
7
8
9
10
11
12
public ScrollViewGroup(Context context) {
this(context, null);
}

public ScrollViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public ScrollViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
}

2.重写View的computeScroll()方法,下面的代码基本是不会变化的:

1
2
3
4
5
6
7
8
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}

3.调用startScroll()方法,startX和startY为开始滚动的坐标点,dx和dy为对应的偏移量:

1
2
mScroller.startScroll (int startX, int startY, int dx, int dy);
invalidate();

完整代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
}
}

原理分析

首先看构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
mFinished = true;
if (interpolator == null) {
mInterpolator = new ViscousFluidInterpolator();
} else {
mInterpolator = interpolator;
}
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
mFlywheel = flywheel;

mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}

在构造器中做的主要就是指定了插补器,如果没有指定插补器,那么就用默认的ViscousFluidInterpolator。

接着看Scroller的startScroll():

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

startScroll()方法什么都没有做,只是保存了我们传递进去的几个参数,方法参数的含义通过名字就可以看出:

  • startX–起始滑动的X坐标
  • startY–起始滑动的Y坐标
  • dx–X方向上的滑动偏移距离
  • dy–Y方向上的滑动偏移距离
  • duration–完成滑动的时间

注意:要注意,不管是scrollTo()还是scrollBy()方法,滚动的都是该View内部的内容,如果LinearLayout中包含Button,那么LinearLayout中的内容就是Button,如果要让Button移动,需要让LinearLayout调用scrollTol或者是scrollBy方法,如果你直接调用button的scroll方法的话,那结果一定不是你想看到的。

startScroll()并没有让View滑动,引起滑动的是invalidate()这行代码。这句代码引起了View的重新绘制,而View的draw方法中又调用了computeScroll()方法。而View类里面的computeScroll()是一个空的方法,需要我们去实现:

1
2
3
4
5
6
7
8
/**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
* object.
*/
public void computeScroll() {
}

在关于Scroller的使用中我们重写了computeScroll()方法,在computeScroll()方法中首先调用了computeScrollOffset()方法进行判断,然后调用scrollTo(mScroller.getCurrX,mScroller.getCurrY())方法移动,最后调用postInvalidate()方法进行重绘,这样computeScroll()方法就会重新被调用,重复执行,循环停止取决于computeScrollOffset()方法的返回值,computeScrollOffset()方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}

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;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}

mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;

mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);

mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);

if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}

break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}

这个方法的返回值若返回true则说明Scroller的滑动没有结束;若返回false说明Scroller的滑动结束了。内部的代码中先是计算出了已经滑动的时间,若已经滑动的时间小于总滑动的时间,则说明滑动没有结束,不然就说明滑动结束了,设置标记mFinished=true。而在滑动未结束里面又分为了两个mode,不过这两个mode都干了差不多的事,大致就是根据刚才的时间timePassed和插补器来计算出该时间点滚动的距离mCurrX和mCurrY。也就是上面第二步的mScroller.getCurrX(),mScroller.getCurrY()的值,在第二部中调用scrollTo()方法滚动到指定点(即上面的mCurrX, mCurrY)。之后又调用了postInvalidate(),让View重绘并重新调用computeScroll()以此循环下去,一直到View滚动到指定位置为止,至此Scroller滚动结束。

Scroller的原理图:
此处输入图片的描述

参考资料:
《Android群英传》
深入解析Scroller滚动原理
Scroller的原理与简单使用
Android Scroller完全解析,关于Scroller你所需知道的一切

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器