弹性滑动的实现与工作原理
使用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(); } }
1.通过如下的startScroll源码,我们可以看出startScroll方法仅仅是保存了传递的几个参数,并没有进行滑动操作
/**
* 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表示的是滑动时间
- 可以发现,调用startScroll方法并不能实现滑动,实现滑动的是下面的invalidate方法
- invalidate方法会导致View重绘,在View的draw方法中又会去调用computeScroll方法,这个方法是空实现,需要用户自己实现(典型代码中已实现)
- computeScroll方法会去向Scroller获取当前的scrollX和scrollY,然后通过scrollTo方法实现滑动
- 接着又会调用postInvalidate()来进行第二次重绘(同第一次一样),computeScroll()——>获取当前scrollX和scrollY——>scrollTo()滑动到新位置,如此反复,直到整个滑动过程结束(mScroller.computeScrollOffset()为false)
2.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;
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;
}
- 这个方法根据时间的流逝来计算出当前的scrollX和scrollY的值,返回true表示滑动还未结束,返回false表示滑动已经结束
- Scroller本事并不能实现View的滑动,需要配合View的computeScroll才能完成弹性滑动的效果,不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller可以得出View当前的滑动位置,然后通过scrollTo方法来完成View的滑动
- View的每一次重绘都会导致View进行小幅度的滑动,多次小幅度滑动就组成了弹性滑动
通过动画实现
利用动画的特性,模仿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();
- 在动画的每一帧到来时获取动画完成的比例,然后根据这个比例计算出当前View所要滑动的距离
- 这个的滑动针对的是View的内容而非View本身
使用延时策略
核心思想:通过发送一系列延时消息从而达到一种渐近式的效果,比如使用postDelayed方法,可以通过延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,就可以实现弹性滑动的效果;对于Sleep方法来说,通过在while循环中不断地滑动View和sleep,就可以实现弹性滑动效果
如下代码用发送延时消息实现了弹性滑动
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;
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;
}
}
}