1. 使用 Scroller
Scroller 的典型用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18Scroller scroller = new Scroller(mContext);
// 缓慢滚动到指定位置
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int deltaX = destX - scrollX;
// 1000 ms 内滑向 destX,效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, deltaX, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}Scroller 的工作原理
startScroll()
方法实现1
2
3
4
5
6
7
8
9
10
11
12
13public 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 本身位置的改变
invalidate()
方法会导致 View 重绘,在 View 的draw()
方法中又会去调用computeScroll()
方法,正是因为这个自己实现的在 View 中是空实现的computeScroll()
方法,View 才能实现弹性滑动- 当 View 重绘后会在
draw()
方法中调用computeScroll()
,而computeScroll()
又会去向 Scroller 获取当前的 scrollX 和 scrollY- 然后通过
scrollTo()
方法实现滑动 - 接着又调用
postInvalidate()
方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致computeScroll()
方法被调用 - 然后继续向 Scroller 获取当前的 scrollX 和 scrollY,并通过
scrollTo()
方法滑动到新的位置,如此反复,直到整个滑动过程结束
- 然后通过
computeScrollOffset()
方法实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public booean computeScrollOffset() {
...
int timePassed = (int) (AnimationUtils.currentAnimationTimeMilis() - 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 则表示滑动已经结束
- 根据时间的流逝来计算出当前的 scrollX 和 scrollY 的值
原理总结
- Scoller 本身并不能实现 View 的滑动,它需要配合 View 的
computeScroll()
方法才能完成弹性滑动的效果 - 它不断地让 View 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得出 View 当前的滑动位置,知道了滑动位置就可以通过
srollTo()
方法来完成 View 的滑动 - View 的每一次重绘都会导致 View 进行小幅度的滑动,多次的小幅度滑动就组成了弹性滑动,类似电影的帧放映
- Scoller 本身并不能实现 View 的滑动,它需要配合 View 的
2. 通过动画
动画本身就是一种渐进的过程,因此通过它来实现的滑动天然就具有弹性效果
1
2// Demo: 让一个 View 的内容在 100ms 内向左移动 100 像素
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();利用动画的特性来实现一些动画不能实现的效果
1
2
3
4
5
6
7
8
9
10
11
12// Demo: 模仿 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();
mButton.scrollTo(startX + (int) (deltaX * fraction), 0);
}
});
animator.start();- 上面的动画本质上没有作用于任何对象上,它只是在 1000ms 内完成了整个动画过程
- 利用这个特性,就可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前 View 所要滑动的距离,这里的滑动针对的是 View 的内容而非 View 本身
- 这个方法的思想和 Scroller 类似,都是通过改变一个百分比配合
scrollTo()
方法来完成 View 的滑动 - 采用这种方法除了能完成弹性滑动以外,还可以实现其他动画效果,完全可以在
onAnimationUpdate()
方法中加上想要的其他操作
3. 使用延时策略
核心思想:通过发送一系列的延时消息从而达到一种渐进式的效果
实现方式:
- 使用 Handler
- 使用 View 的
postDelayed()
方法:通过它来发送一个消息,在消息中进行 View 的滑动,如果接连不断地发送这种延时消息,就可以实现弹性滑动的效果 - 使用线程的
sleep()
方法:通过在 while 循环中不断滑动 View 且sleep()
,就可以实现弹性滑动的效果
Demo:
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// 使用 Handler,在大约 1000ms 内将 View 的内容向左移动 100 像素
// 之所以说大约,是因为采用这种方法无法精确定时,因为系统的消息调度也需要时间且所需时间不定
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;
"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);
mButton.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
default:
break;
}
}
}