Android平滑移动——Scroller类研究

Scroller是Android中View平滑移动的一个辅助类,对于刚接触Scroller的人群来说它可能难以理解:
  1、它是怎样滑动View的(如何与View关联的)?
  2、又是谁触发了它?

其实要分析这两个问题,主要还得从View的绘制流程开始分析:
关于View的绘制流程,网上资料众多,基本上相差无几,这里就不再阐述,下面提取下解析Scroller功能的必要的几个View的绘制方法:

scrllTo()/scrollBy() ---> invalidate()/postInvalidate() ---> computeScroll();(这个流程我们可以分析源码得到)。scrllTo()/scrollBy()是view移动的两个方法;它会更新View的新的坐标点,然后调用invalidate/postInvalidate方法刷新view; 滑动完成后再调用computeScroll()方法;computeScroll()是View.java的一个空的方法,需要由我们去实现处理。

根据上面的流程我们想想,当我们在computeScroll()方法中调用scrollTo/scrollBy的时候,会发生什么?

  答:很简单,这样就会达成一个死循环,在同一个位置不断的刷新View,只是由于系统缓存与一些优化机制,我们看不出来它在刷新而已!

再当我们在computeScroll()方法中调用scrollTo/scrollBy的时候,同时不断的改变Y坐标的值又会发生什么?

  答:跟上面一样,会死循环刷新View,只是由于Y坐标不断的在变化,导致了View根据Y坐标变化规律上下移动,这样一来,如果Y坐标的变化是有规律的,是慢慢向下移动的,那这就达到了我们今天要研究的效果----平滑移动了;而这里我们今天要谈的Scroller就是这样一个工具类,给我们提供有规律变化的坐标的工具类;嘿嘿,似乎发现了什么....

  有种拨云见日的感觉啊,原来平滑移动如此easy,那么与其说Scroller是Android中View平滑移动的一个辅助类,不如直接说Scroller是一个计算坐标的工具类。其实Scroller与View的滑动是没有关系的,它只是计算在动画执行某个时间所在的某个位置的坐标,这样就形成了坐标路线,再view根据坐标路线循环invalidate在界上新显示,就形成了我们看到的平滑移动了。

知道了Scroller的工作原理,下面根据源码分析一下Scroller类常用的几个方法:
1、startScroll()方法:
  源码:

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

  简单到让人无法相信,仅仅是设置了一下动画开始时间,起始坐标,终点坐标、时间倒数(仅仅是方便计算而已)等变量而已;其实这样我们的动画可以说已经开始了,只是没有根据坐标绘制到界面上而已;因为它这里保存了开始时间,当平滑开始的时候,Scroller就可以根据滑动的时间差来计算当前坐标应该处的位置,View根据坐标invalidate就可以滑动了;

  当然这里影响到坐标计算的还有一个就是加速器,在重载的构造方法方法 public Scroller(Context context, Interpolator interpolator) 里面有详述:这里我们也可以自定义自己的加速器,具体原理与如何自定义这里就不阐述了;


2、computeScrollOffset()方法:

  

    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                float x = timePassed * mDurationReciprocal;
    
                if (mInterpolator == null)
                    x = viscousFluid(x); 
                else
                    x = mInterpolator.getInterpolation(x);
    
                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);
                final float t_inf = (float) index / NB_SAMPLES;
                final float t_sup = (float) (index + 1) / NB_SAMPLES;
                final float d_inf = SPLINE[index];
                final float d_sup = SPLINE[index + 1];
                final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);
                
                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;
    }

看代码主要当平滑没有完成的情况下,计算出当前滑动的坐标位置,如果平滑完成了,就不需要计算了;这里有一个SCROLL_MODE和FLING_MODEL两种滑动模式,这里是SCROLL_MODE手动拖动平滑模式,FLING_MODEL是由于手指滑动速率来判断惯性滑动。一般这个方法执行完成之后,根据返回值判断是否需要invalidate/postinvalidate,再根据Scroller计算好的坐标值,View将scrollTo/scrollBy到新的坐标位置;

3、fling()方法:

    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 = FloatMath.sqrt(dx * dx + dy * 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 = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
     
        mVelocity = velocity;
        final double l = Math.log(START_TENSION * velocity / ALPHA);
        mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0)));
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;

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

        int totalDistance =
                (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l));
        
        mMinX = minX;
        mMaxX = maxX;
        mMinY = minY;
        mMaxY = maxY;

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

滑动,这个滑就跟GestureDetector.OnGestureListener这个接口中的onFling事件一样,根据滑动速率来判断一些事件。主要处理一些一些惯性坐标变化(惯性行为)。用得比较少;

4、abortAnimation()方法:

    public void abortAnimation() {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }

看源码,停止动画(就是设置一个标记而已,不再计算坐标的变化值)

在代码的开始外看到这样一段静态代码块:

  

    static {
        float x_min = 0.0f;
        for (int i = 0; i <= NB_SAMPLES; i++) {
            final float t = (float) i / NB_SAMPLES;
            float x_max = 1.0f;
            float x, tx, coef;
            while (true) {
                x = x_min + (x_max - x_min) / 2.0f;
                coef = 3.0f * x * (1.0f - x);
                tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x;
                if (Math.abs(tx - t) < 1E-5) break;
                if (tx > t) x_max = x;
                else x_min = x;
            }
            final float d = coef + x * x * x;
            SPLINE[i] = d;
        }
        SPLINE[NB_SAMPLES] = 1.0f;

        // This controls the viscous fluid effect (how much of it)
        sViscousFluidScale = 8.0f;
        // must be set to 1.0 (used in viscousFluid())
        sViscousFluidNormalize = 1.0f;
        sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
    }

这一段代码让我纠结了好久,照我的理解应该是把惯性滑动模式中的滑动距离比率(不好描述)分成100份,精确到0.00001,在惯性滑动的时候根据当前时间比率来计算当前坐标处于的位置;

其实说到这里,感觉Android里面的动画,其实就是对于坐标的精确计算,这里只是简单的平滑滑动,有能力者我们也可以根据自己的需求写出自己的坐标计算类来达到我们的需求;

Scroller的使用方法这里不阐述了,这里引用一下另一们童鞋的博文:http://ipjmc.iteye.com/blog/1615828。这里有一个很好的demo,可以参考学习;欢迎留言讨论。

原文地址:https://www.cnblogs.com/PDW-Android/p/3653253.html