比较生硬地滑动一个View,用户体验实在是太差了,所以我们需要实现优雅的弹性滑动效果。那么如何实现弹性滑动呢?
主要思想:把一次滑动分成若干次小距离滑动,并在一定时间内完成以达到弹性滑动的效果。
具体实现方法有很多,例如使用Scroller、Handler#postDelay和Thread#sleep。
一、使用Scroller
Scroller典型用法
//创建实例
Scroller scroller = new Scroller(context);
//设置滑动参数
scroller.startScroll(scroller.getStartX(), scroller.getStartY(), 1000, 1000, 1000);
//重绘View
invalidate();
//重写computeScroll方法
@Override
public void computeScroll() {
if (mScroller != null) {
if (mScroller.computeScrollOffset()) {
mScrollX = mScroller.getCurrX();
mScrollY = mScroller.getCurrY();
scrollTo(mScrollX, mScrollY );
postInvalidate(); // So we draw again
}
}
}
我们先描述上述代码主要逻辑:当我们构造Scroller对象并调用它的startScroll方法时,Scroller内部其实什么也没做,它只是保存我们传递的几个参数,我们根据源码看看这些参数的含义
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
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内容的滑动,而不是它本身位置的改变。
既然startScroll方法没有做与滑动相关的动作,那么Scroller是怎么让View滑动的呢?答案就是invalidate方法,invalidate方法会导致View重绘,View重绘时draw方法中又会去调用computeScroll方法,这是一个空方法,需要我们自己去实现,这个方法才是View能够实现弹性滑动的关键所在。具体过程是这样的:
- View重绘时在draw方法中调用computeScroll方法,computeScroll方法会向当前的Scroller获取新的mScrollX、mScrollY;
- 通过scrollTo(mScrollX, mScrollY )实现滑动;
- 调用postInvalidate方法再次重绘,绘制过程与第一次一样;
- 如此反复直到滑动到指定位置。
开始重复绘制当然是有条件的,难道要任由View一直重绘下去?我们显然不能这样做。阅读TextView源码我们发现computeScroll方法开始执行之前有这么一个判断语句"if (mScroller.computeScrollOffset()) ",我们来看看computeScrollOffset方法的实现。
/**
* 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;
……
}
}
……
return true;
}
我们可以看到,方法内部是根据时间流逝的百分比来计算出mStartX 、mStartY变化的百分比,从而最终计算出mCurrX、mCurrY的值,从而确定新位置。方法computeScrollOffset返回true时表示滑动未结束,否则表示滑动结束。
二、通过动画
动画本身就是一个渐变的过程,所以我们可以用它来实现弹性效果。我们尝试着使用动画让一个View的内容在1秒内向右移动100像素:
ObjectAnimator.ofFloat(hello, "translationX", 0, 100)
.setDuration(1000).start();
像上面这样通过动画来实现View的内容滑动还不是我想介绍的重点,我们还可以利用动画的特性,配合scrollTo()方法来实现一些动画不能实现的效果,例如模仿Scroller实现View的弹性滑动。用法如下:
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedFraction = animation.getAnimatedFraction();
Log.d(TAG, "onAnimationUpdate: " + animatedFraction);
hello.scrollTo((int) (-100 * animatedFraction), 0);
}
});
valueAnimator.start();
上面定义的动画valueAnimator 本质上并没有作用在任何对象上,它只是在1000ms内完成了整个动画的过程。
这个方法的思想其实和Scroller非常类似,都是将一次滑动分割为多次小距离滑动:通过一个渐变的百分比animatedFraction 计算出每次需要滑动的距离,然后配合scrollTo完成整个滑动过程。
AnimatorUpdateListener监听事件作用非常大,我们可以在它的onAnimationUpdate方法内实现各种自定义滑动效果。
三、使用延时策略
核心思想:发送一系列延时消息,然后在这些消息中进行View的内容滑动,从而达到弹性滑动的效果。
我们可以使用Handler、View的postDelayed方法或者使用线程的sleep方法。下面以Handler为例来实现弹性滑动,其他方法思想都是类似的。
private static final int SCROLL_TO = 1;
private static final int SCROLL_COUNT = 50;
private int count;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SCROLL_TO:
count++;
if (count <= SCROLL_COUNT) {
//计算百分比
float fraction = count / (float)SCROLL_COUNT;
Log.d(TAG, "handleMessage: "+fraction);
//根据百分比计算滑动的距离
int scrollX = (int) (fraction * -500);
hello.scrollTo(scrollX, 0);
//发送第count+1次消息
sendMessageDelay();
}
}
}
};
注意:这里主要目的是为了介绍延迟策略,所以没有考虑Handler内存泄漏的问题,如何正确使用Handler请参考Android中常见的内存泄漏 & 解决方案。
四、总结
上面几种实现弹性滑动的方法,都只是简单介绍了它们的实现思想,在实际使用中肯定比这要复杂得多,我们需要对它们进行灵活地扩展,这样它们才能在开发工作中发挥更大的作用。
参考
《Android开发艺术探索》
网友评论