浅谈Scroller

作者: Longalei | 来源:发表于2017-04-13 19:50 被阅读136次

李达康书记在给大风厂员工讲话时,后面的墙上写道:“努力能把事情做好,而用心却能把事情做对。”Scroller这个有意思的东西,我们现在就去来简单的了解了解它。

李达康书记说过要好好写代码才能娶媳妇.jpg

Scroller的由来

当谈到View的滑动的时候,我们知道通常有三种方式:
1.利用View的scrollTo()/scrollBy()方法;
2.使用动画;
3.改变View的LayoutParams;
在实现View的滑动的时候为什么会与Scroller有关系了?好吧,原因来了,因为在View的滑动中有一个名词是叫弹性滑动,而Scroller就是一个弹性滑动对象。言外之意就是用来实现弹性滑动的一个帮助类。

Scroller的组成

打开Scroller类,我们可以发现其就是一个单一类。关于类的介绍说其是一个封装的滑动类,我们可以用Scroller的对象或通过OverScroller收集需要的数据来实现一个弹性滑动。举个例子说吧,要响应一个滑动手势,Scroller会随着时间的推移去跟踪这个滑动的偏移量,但是它们并不会自动的把这些偏移的位置应用在你的View上,需要我们自己去得到这些值并且以一定的速率应用在新的坐标上,这样的话就会使滑动动作看起来更平滑些。
Scroller这个类比较简单,代码也就600行不到。通过对整个类的观察我们可以知道有以下主要成员变量与方法。


Scroller的主要成员变量与方法.png

通过对这个类的大致观看一下,除了一些get()方法,我们可以把注意力多放在set()与startScroll()方法上。

研究Scroller探究其工作机制

接下来我们就通过代码仔细研究Scroller到底是怎么进行工作的?上面我们就说到可以先把注意力放在set()方法与startScroll()方法上。
我们先研究下computeScrollOffset()这个方法,以下是Scroller的源代码:

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

我们对这个方法进行分析:mFinished这个成员变量用来记录scroller是否已经结束滑动。当滑动结束后,返回false,当还在滑动的时候返回true。而通过1.2行代码我们就可以清楚mFinished的重要性,只要改变mFinished就可以直接结束滑动。所以Scroller类中有一个方法可对mFinished进行控制,代码如下:

 /**
     * Force the finished field to a particular value.
     *  
     * @param finished The new finished value.
     */
    public final void forceFinished(boolean finished) {
        mFinished = finished;
    }

紧接着我们继续研究代码,有一个局部变量timePassed用来记录当前时间与开始时间mStartTime的时间差,好,问题来了,mStartTime这个字段什么时候被赋值就意味着滑动从什么时候开始,那么mStartTime什么时候赋值了?我们可以发现:mStartTime在startScroll()与fling()方法中被赋值,那我们先对这两个方法研究,以下是源代码及发现:

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;
    }
public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) {
        // Continue a scroll or fling in progress
        if (mFlywheel && !mFinished) {
            float oldVel = getCurrVelocity();

            float dx = (float) (mFinalX - mStartX);
            float dy = (float) (mFinalY - mStartY);
            float hyp = (float) Math.hypot(dx, dy);

            float ndx = dx / hyp;
            float ndy = dy / hyp;

            float oldVelocityX = ndx * oldVel;
            float oldVelocityY = ndy * oldVel;
            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
                velocityX += oldVelocityX;
                velocityY += oldVelocityY;
            }
        }

        mMode = FLING_MODE;
        mFinished = false;

        float velocity = (float) Math.hypot(velocityX, velocityY);
     
        mVelocity = velocity;
        mDuration = getSplineFlingDuration(velocity);
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;

        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;

        double totalDistance = getSplineFlingDistance(velocity);
        mDistance = (int) (totalDistance * Math.signum(velocity));
        
        mMinX = minX;
        mMaxX = maxX;
        mMinY = minY;
        mMaxY = maxY;

        mFinalX = startX + (int) Math.round(totalDistance * coeffX);
        // Pin to mMinX <= mFinalX <= mMaxX
        mFinalX = Math.min(mFinalX, mMaxX);
        mFinalX = Math.max(mFinalX, mMinX);
        
        mFinalY = startY + (int) Math.round(totalDistance * coeffY);
        // Pin to mMinY <= mFinalY <= mMaxY
        mFinalY = Math.min(mFinalY, mMaxY);
        mFinalY = Math.max(mFinalY, mMinY);
    }

我们可以发现Scroller提供两种滑动模式,并且在调用这两个方法的时候并不会对View进行滑动,而只是对一些成员变量进行赋值,例如:其中的一个参数就是开始的时间,并且我们可以明白Scroller它并没有对View的对象进行操作,这有点我们不生产水,我们只是大自然的搬运工啊的味道。哈哈。继续,Scroller又是一个滑动对象。那么我们可以猜想我不动你,你要滑动,那么只有你自己动了。要不然没办法了。这个思路没毛病的。那么View它怎么自己通过Scroller的一系列赋值自己动了,我知道你已经想到了,那就是让它自己进行重绘了。所以这就更加验证了Scroller它真的只是一个来辅助实现弹性滑动的帮助类了,它不是主力。但是它却往外提供计算好了的一定时间内的改变的当前位置坐标。这基本上是Scroller的一个工作流程了。

使用Scroller来实现View的弹性滑动

在使用Scroller来实现弹性滑动时,我们需要了解一个方法,不过与其说是一个方法,还不如说是它的一个老搭档,何况官方也是这么说的,是哪个方法了?那就是computeScroll()这个方法,computeScroll()这个方发是在View这个顶级父类中的,它是一个空实现,主要是用来给子类进行重写的。我们可以看看这个方法在View类中的介绍:

/**
     * 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() {
    }

typically be done 这下你明白这两货之间有多配合了吧,下面的一个Demo则是它们普遍的一个配合方法。

public class MoveView extends AppCompatTextView {
    private static final String TAG = "MoveView";
    private Scroller mScroller;

    public MoveView(Context context) {
        super(context);
    }

    public MoveView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

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

    public void zoomIn(){
        mScroller.forceFinished(true);
        mScroller.startScroll(0,0,100,100,1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller != null) {
            if (mScroller.computeScrollOffset()) {
                int mScrollX = mScroller.getCurrX();
                int mScrollY = mScroller.getCurrY();
                scrollTo(mScrollX,mScrollY);
                postInvalidate();  // So we draw again
            }
        }
    }
}

外部调用zoomIn()方法就可以实现将此View的内容在1秒内水平距离和竖直距离分别移动100个像素。其中调用的invalidate()函数会使整个View进行重绘,当然也会调用onDraw()方法。这是无疑问的,但是跟我computeScroll()有什么联系了?既然亲测能够实现弹性滑动,那么他们之间肯定是有联系的,什么联系我们继续看源码!这个onDraw()跟computeScroll()方法一样在View父类中都是一个空实现。且这个onDraw(Canvas canvas)方法只在View类的void draw(Canvas canvas)中调用且执行这个绘制有几个必须执行的绘图步骤。这里我简要说一下:

  • 绘制背景
  • 保存画布图层以备褪色(有必要的情况下)
  • 绘制视图的内容
  • 绘制衰减边并恢复图层(有必要的情况下)
  • 画装饰(例如:滚动条)
    同时,我们通过View的源码发现,在View的绘制过程中,其中也对computeScroll()方法进行了调用。所以并不是说invalidate()函数会使onDraw()方法调用从而使computeScroll()方法进行调用,他们之间是没有包含联系的,有的只是在绘制的过程的某一个阶段同时被调用了。所以这一点理解我觉得很重要。

总结

其实这个通过Scroller这种方式来实现弹性滑动的思路很高级呀。值得我们去学习。Be in a Googley Mood,总的来说,Scroller就是燃料补给器,invalidate()函数就是发动机整个系统。scrollTo()就是轮子。View就是汽车。汽车要想动起来,就要燃料给其提供燃料值,然后发动机再运行整个系统让轮子动起来从而汽车行驶起来了。可能比喻的不够恰当,可是我就是这么理解的,如果有更好的想法,记得告诉我哦。

相关文章

  • 浅谈Scroller

    李达康书记在给大风厂员工讲话时,后面的墙上写道:“努力能把事情做好,而用心却能把事情做对。”Scroller这个有...

  • Scroller弹性滑动

    Scroller弹性滑动经典代码: /** 构建Scroller对象 */ Scroller mScroller ...

  • Android学习整理 - 15- Scroller

    Android Scroller完全解析,关于Scroller你所需知道的一切 Scroller 构造函数 Scr...

  • Android中Scroller的使用及原理解析

    1. Scroller的使用 1.1 构造Scroller 可以看到Scroller有2个构造器,其中第二个构造器...

  • Scroll的逻辑

    问题: Scroller 的滑动原理 初始化 mScroller = new Scroller(c...

  • 2018-11-26

    Scroller的使用 1、初始化Scroller 2、重写computeScroll()方法 3、调用start...

  • 3.3 弹性滑动

    1. Scroller原理分析 经典使用 首先看Scroller.startScroll方法 这个方法只是保存了一...

  • 初识Scroller

    Scroller Scroller是一个专门用于处理滚动效果的工具类,例如ViewPager,ListView等控...

  • Scroller的使用

    scroller定义 startScroll computeScroll

  • Scroller

    在Android开发中有多种方式实现View的滑动,常见的有三种如下: 不断地修改View的LayoutParam...

网友评论

    本文标题:浅谈Scroller

    本文链接:https://www.haomeiwen.com/subject/rdauattx.html