Scroller的原理与简单使用

Scroller是Android中处理滑动效果的核心类。它通过配合View所提供的scrollTo()和scrollBy()这两个方法,就能够使用多种多样的滑动效果。网上也有很多文章介绍关于Scroller的使用,那么在这篇文章中,我尽可能缩减篇幅将Scroller的原理和实现介绍一下。

1. scrollTo()和scrollBy()

首先介绍一下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();        
    }    
  }  
}

scrollTo()是让当前的View能够平移到所给定的位置,然后通过起始位置和结束位置的坐标,根据onScrolledChanged()方法的调用最终完成绘制。

/** 
 * 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()而完成的,唯一的区别在于scrollBy()是根据当前已经位移的坐标为基础再继续进行位移。也就是说scrollTo()实现的是输入参数的绝对滑动,而scrollBy()实现的是输入参数的相对滑动。

另外,scrollTo()和scrollBy()方法中都提到了mScrollX和mScrollY两个属性,这两个属性可以分别通过getScrollX()和getScrollY()获得。

《Scroller的原理与简单使用》 mScrollXY.png

如上图,我们得到以下结论:
(1)View的左边界位于View内容左边界的左侧时,mScrollX < 0;反之,mScrollX > 0。
(2)View的上边界超过View内容上边界时,mScrollY < 0;反之,mScrollY > 0。

2. Scroller的滑动偏移计算

通过阅读Scroller的源码我们发现,Scroller虽然作为View滑动的重要依据,但是却不能真正操纵View滑动,他是通过旧的坐标值和新的坐标值算出偏移距离,配合View的computeScroll()方法完成滑动操作。

/** 
 * 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 – 起始滑动的X坐标
startY – 起始滑动的Y坐标
dx – X方向上的滑动偏移距离
dy – Y方向上的滑动偏移距离
duration – 完成滑动的时间

通过代码我们可以看出,Scroller的startScroll()方法作为View的滑动依据只是单纯的对于变量进行和一系列的赋值操作。

3. 实现滑动效果

通过上面的startScroll()方法的分析,我们了解到该方法只是针对View滑动偏移的计算和赋值工作,并没有真正的执行滑动效果。而真正实现滑动的是View类的computeScroll()方法完成的。

在源码中,我们可以看到View类的computeScroll()方法只是一个空实现,因此我们要完成滑动效果需要复写此方法。如下图:

《Scroller的原理与简单使用》 QQ截图20160819145459.png

在方法中,我们利用Scroller的computeScrollOffset()方法去判断是否View有滑动事件的发生,如果有,即可通过scrollTo()方法完成滑动偏移。

    /**
     * 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;
    }

4. invalidate()

computeScroll()方法并非凭空实现的,它需要调用invalidate()去进行View的重新绘制,invalidate()的源码中有对于computeScroll()方法的调用。

网上关于Scroller的源码讲解也有很多,我也是借鉴别人的列子通过自己的理解完成的本文,因此不能完全说的上是原创。也希望大家能够理解。

献上代码
ScrollDemo
小结,通过Scroller我们可以实现很多弹性滑动的效果。

    原文作者:NeroIsNovice
    原文地址: https://www.jianshu.com/p/3cc780dc26b5
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞