Scroller滑动原理--滑动动画驱动原理+滑动不到位误差分析

记录:这里主要记载最近学习的结合Scoller实现View的滑动,从应用和源码的角度去分析一下滑动实现的过程。

1、View的相关支持

    /**
     * The offset, in pixels, by which the content of this view is scrolled
     * horizontally.这里说的是view的内容滑动的偏移量,不是view本身,准确的说
     * 应该是view的内容当前的滑动位置,是一个最终的位置,不是需要滑动的偏移量
     * {@hide}
     */
protectedintmScrollX;
    /**
     * The offset, in pixels, by which the content of this view is scrolled
     * vertically.
     * {@hide}
     */
protectedintmScrollY;
    /**
     * Return the scrolled left position of this view. This is the left  edge of the displayed part of your view. You do not need to 
     * draw any pixels farther left, since those are outside of the frame of * your view on screen.获取上面两个值的方法
     * @return The left edge of the displayed part of your view, in pixels.
     */
    publicfinalintgetScrollX() {
        returnmScrollX;
    }
    /**
     * Return the scrolled top position of this view. This is the top edge * of the displayed part of your view. You do not need to  
     * draw any pixels above it, since those are outside of the frame of your view  on screen.
     * @return The top edge of the displayed part of your view, in pixels.
     */
    publicfinalint getScrollY() {
        returnmScrollY;
    }
    /**
     * 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.
* 设置view的滑动的最终位置,这个操作会导致View.onScrollChanged的调用,同
* 时view将会被invalidate
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    publicvoidscrollTo(intx, inty){}

    /**
     * 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.
     * 设置View滑动的滑动距离,也就是在当前滑动位置的基础上想要再滑动的距离
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    publicvoidscrollBy(intx, inty) {}
上面的注释说的比较清楚,这里主要是View.mScrollX和View.mScrollY,这是两个实现View内容滑动的关键,我们所看到的滑动都是以这两个变量的变化为基础的。

2 简单的滑动

首先写一个简单的应用,介绍一下scrollTo和scrollBy的用法:

//mian.xml

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="bt_screen"
        android:onClick="onClick"/>
LinearLayout>

//activity
    @Override
    publicvoid onClick(View v) {
       // TODO Auto-generated method stub

       switch (v.getId()) {
       case R.id.btn:
           v.scrollBy(20, 0);//每次向左滑动20个像素
           break;
       }
    }

scrollTo和scrollBy的使用方法很简单,直接给出想要滑动到的位置或者想要滑动位置就可以直接滑动了,注意滑动的是里面的内容,而不是View的本身,如果想要滑动某个View,需要将其放在一个Layout里面,然后调用Layout的scrollTo或者scrollBy方法实现滑动。

3 结合Scroller实现滑动动画

上面的图是调用一次scrollTo方法实现的滑动效果,会马上从起始位置滑动到最终位置,下面的图中间多了很多的“滑动中间位置”,这样从起始到最终位置会有一个比较友好的动画效果,具备更好的体验。

“滑动中间位置”帧就是通过修改view的View.mScrollX和View.mScrollY变量,然后反复多次的调用scrollTo方法实现的,为了能够改变View.mScrollX和View.mScrollY,就要用到Scroller这个类,起始完全可以自己来指定每次滑动的策略,手动修改View.mScrollX和View.mScrollY完全是可以实现滑动的,先看一下Scroller这个类:

public class Scroller  {
    private int mMode;

    private int mStartX; //起始坐标点 ,  X轴方向
    private int mStartY; //起始坐标点 ,  X轴方向
    private int mFinalX; //滑动后的最终X轴位置
    private int mFinalY; //滑动后的最终Y轴位置

    private int mCurrX; //当前坐标点  X轴,即调用startScroll函数后,经过一定时间所达到的值
    private int mCurrY; //当前坐标点  Y轴,即调用startScroll函数后,经过一定时间所达到的值
    private long mStartTime;
    private int mDuration;
    private float mDurationReciprocal;
    private float mDeltaX; //应该继续滑动的距离, X轴方向
    private float mDeltaY; //应该继续滑动的距离, Y轴方向
    private boolean mFinished; //是否已经完成本次滑动操作,如果完成则为 true
    private Interpolator mInterpolator; //滑动中使用的插值器

     //开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达坐标为(startX+dx , startY+dy)处
public void startScroll(int startX, int startY, int dx, int dy, int duration){}

 //每次需要新的位置参数的时候调用这个函数,当这个方法返回true说明动画还在进行中,返回false说明滑动动画已经结束
public boolean computeScrollOffset(){}

//获取当前的mCurrX
public final int getCurrX() {
        return mCurrX;
}
//获取当前的mCurrY
public final int getCurrY() {
        return mCurrY;
}
//强制停止当前滑动
public final void forceFinished(boolean finished) {
        mFinished = finished;
}

//中止动画
public void abortAnimation() {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
}

上面列出了Scroller 几个重要的方法和属性,仔细看这个类,并没有继承任何其他的类或者接口,说明这是一个很简单的一般类,看一下这个类的使用方法:

首先,创建一个Scroller对象;
然后,调用Scroller.startScroll(起始x,起始y,滑动dx,滑动dy,滑动持续时间),invalidate请求反复调用computeScrollOffset,然后调用View.scrollTo方法

下面分别来看这三个函数:
(1)Scroller的构造器

   /**
     * Create a Scroller with the specified interpolator. If the  
     *   interpolator is null, the default (viscous) interpolator 
     * will be used. Specify whether or not to support progressive 
     * "flywheel" behavior in flinging.
     */
    public Scroller(Context context, Interpolator interpolator, 
            boolean flywheel){
        mFinished = true;
        if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = 
            computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;

        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    }

这个函数并没有做什么,只是做了一下简单的初始化工作。
(2)Scroller.startScroll(起始x,起始y,滑动dx,滑动dy,滑动持续时间)

 /**
     * 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.
     * 开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,
     * 即到达坐标为(startX+dx , startY+dy)处
     */
    public void startScroll(intstartX, intstartY, intdx, intdy, intduration) {

        //设定SCROLL的mode,在computeScrollOffset很重要
        mMode = SCROLL_MODE;         
        mFinished = false;           //动画结束标记设为false,将要开始新的动画
        mDuration = duration;        //动画的持续时间

        //动画的起始时间
        mStartTime = AnimationUtils.currentAnimationTimeMillis();     
        mStartX = startX;            //滑动动画的起始位置 X方向
        mStartY = startY;            //滑动动画的起始位置 Y方向
        mFinalX = startX + dx;       //滑动动画的结束位置 X方向
        mFinalY = startY + dy;       //滑动动画的结束位置 Y方向
        mDeltaX = dx;                //滑动动画的实际的滑动距离 X方向
        mDeltaY = dy;                //滑动动画的实际的滑动距离 Y方向

        //滑动动画的滑动进程单位,1/总时间
        mDurationReciprocal = 1.0f / (float) mDuration;             
    }

做了一些变量的初始化工作,将模式设置为SCROLL_MODE,并将动画结束标记设为false,准备开始新的计算,另外对一些关键的变量的值做了设置。
(3)Scroller.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) {    
            returnfalse;
        } 
        inttimePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);    
        //当前时间距离滑动起始时间mStartTime的时间间隔
        if (timePassed < mDuration) {      
        //当前时间距离滑动起始时间mStartTime的时间间隔是否已经超过
        //滑动动画设定的时间,动画超时控制
            switch (mMode) {
            caseSCROLL_MODE:
            //重点,timePassed*mDurationReciprocal=timePassed/mDuration,代表目前滑动动画的进程百分比 
                finalfloatx = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);  

                //计算这一帧中,动画滑动的x方向位置,round四舍五入
                mCurrX = mStartX + Math.round(x * mDeltaX); 

                //计算这一帧中,动画滑动的y方向位置,round四舍五入  
                mCurrY = mStartY + Math.round(x * mDeltaY);   
                break;
            caseFLING_MODE:
                break;
            }
        } // end if (timePassed < mDuration)
        else {
            //动画超时的时候,直接将最终的滑动位置设置为最终位置
            mCurrX = mFinalX;         
            mCurrY = mFinalY;
            mFinished = true;
        }
        returntrue;    
    }

这个方法就是根据当前的时间计算出滑动动画已经持续的时间,并利用插值器mInterpolator计算出当前帧应该滑动到的位置,看一下Scroller默认的插值器ViscousFluidInterpolator:

    static class ViscousFluidInterpolator implements Interpolator {
        /** Controls the viscous fluid effect (how much of it). */
        privatestaticfinalfloatVISCOUS_FLUID_SCALE = 8.0f;
        privatestaticfinalfloatVISCOUS_FLUID_NORMALIZE;
        privatestaticfinalfloatVISCOUS_FLUID_OFFSET; 
        static {
            // must be set to 1.0 (used in viscousFluid())
            VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
            // account for very small floating-point error
            VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
        } 
       //涉及到e的复杂的数学公式
        privatestaticfloat viscousFluid(floatx) {
            x *= VISCOUS_FLUID_SCALE;
            if (x < 1.0f) {
                x -= (1.0f - (float)Math.exp(-x));
            } else {
                floatstart = 0.36787944117f;   // 1/e == exp(-1)
                x = 1.0f - (float)Math.exp(1.0f - x);
                x = start + x * (1.0f - start);
            }
            returnx;
        }  
  /**
     * Maps a value representing the elapsed fraction(分数,小数;
     * elapsed fraction:过去部分) of an animation to a value that 
     * represents the interpolated fraction. This interpolated value is
     * then multiplied by the change in value of an animation to 
     * derive the animated value at the current elapsed animation time.
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end ,说白了就是目前动画的已经进行的百分比,以时间为标准计算
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
        @Override
        public float getInterpolation(floatinput) {
            final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
            if (interpolated > 0) {
                return interpolated + VISCOUS_FLUID_OFFSET;
            }
            return interpolated;
        }
    }

仔细看一下Scroller就是一个简单的数值运算,以动画当前进行的百分比为输入,根据一定的公式(插值器)计算下一帧动画的位置信息:

nextPos = f(progress) 下一帧的位置 = Scroller.computeScrollOffset(滑动进度)
上面分析完了Scroller的作用,就是进行数值运算,下面的问题就是这些计算出来的值该如何运用到View的动画上面?

使用方法:

  public class MultiViewGroup extends ViewGroup {
    public int mDurationTimeMs = 1000;
    Scroller mScroller = new Scroller();    //这里使用默认的插值器
    publicvoid startMove(){
       mScroller.startScroll(0, 0, 720, 0,mDurationTimeMs);
       invalidate();
    }
    @Override
    publicvoid computeScroll() {         
       boolean needInvalidate = false;
       if (mScroller.computeScrollOffset()) {  // 如果返回true,表示动画还没有结束,会进行一次新的scrollTo滑动,如果返回true说明动画没有结束,并且下一帧滑动的位置已经在computeScrollOffset计算好
           // 产生了动画效果每次滚动一点
           scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 这里getCurrX或Y取出Scroller.computeScrollOffset计算出来的滑动位置信息,然后进行scrollTo滑动
           invalidate();
       }
    }
}

上面只要调用MultiViewGroup对象的startMove方法就可以将整个屏幕向左滑动移动720px,使用起来还是很简单的,下面提出两个关键的问题:
(1)整个动画具体是怎么完成的,如何驱动的?如何维持的?如何结束的?
(2)根据最前面的scrollTo的注释,可以看到scrollTo会主动去调用invalidate()方法,但是为什么后面又要调用一次invalidate方法,网上的demo里面都说不调用会有误差,根据实际的测试,确实会出现误差,明明已经调用了invalidate,然后重复一次invalidate就没有误差了,真是奇怪,原因到底是什么?

问题1:整个动画具体是怎么完成的,如何驱动的?如何维持的?如何结束的?
这个问题网上很多地方都是有答案的,但是总是将的不清楚,invalidate具体做了什么,下面会再写一篇记录性的文章。总的来说invalidate就是进行了一次重绘事件,最终会导致ViewRootImpl中performTraversals方法的调用,然后将draw事件传递到PhoneWindow$DecorView.draw,最终上传到每一个控件,现在只要知道invalidate方法会导致View进 I/System.out(25777):com.qin.scrollerview.MultiViewGroup.computeScroll(TestTextView.java:49)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14043)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.View.draw(View.java:14846)
I/System.out(25777): android.view.ViewGroup.drawChild(ViewGroup.java:3405)
I/System.out(25777): android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14051)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.View.draw(View.java:14846)
I/System.out(25777): android.view.ViewGroup.drawChild(ViewGroup.java:3405)
I/System.out(25777): android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14051)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.View.draw(View.java:14846)
I/System.out(25777): android.view.ViewGroup.drawChild(ViewGroup.java:3405)
I/System.out(25777): android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14051)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.View.draw(View.java:14846)
I/System.out(25777): android.view.ViewGroup.drawChild(ViewGroup.java:3405)
I/System.out(25777): android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
I/System.out(25777): android.view.View.draw(View.java:15125)
I/System.out(25777): android.widget.FrameLayout.draw(FrameLayout.java:592)
I/System.out(25777): com.android.internal.policy.impl.PhoneWindow$DecorView.draw(PhoneWindow.java:2646)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14056)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:266)
I/System.out(25777): android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:272)
I/System.out(25777): android.view.ThreadedRenderer.draw(ThreadedRenderer.java:311)
I/System.out(25777): android.view.ViewRootImpl.draw(ViewRootImpl.java:2523)
I/System.out(25777): android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2361)
I/System.out(25777): android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1992)
I/System.out(25777): android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1078)
I/System.out(25777): android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5810)
I/System.out(25777): android.view.Choreographer$CallbackRecord.run(Choreographer.java:818)
I/System.out(25777): android.view.Choreographer.doCallbacks(Choreographer.java:617)
I/System.out(25777): android.view.Choreographer.doFrame(Choreographer.java:583)
I/System.out(25777): android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:804)
I/System.out(25777): android.os.Handler.handleCallback(Handler.java:739)
I/System.out(25777): android.os.Handler.dispatchMessage(Handler.java:95)
I/System.out(25777): android.os.Looper.loop(Looper.java:135)
I/System.out(25777): android.app.ActivityThread.main(ActivityThread.java:5249)
I/System.out(25777): java.lang.reflect.Method.invoke(Native Method)
I/System.out(25777): java.lang.reflect.Method.invoke(Method.java:372)
I/System.out(25777): com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:907)
I/System.out(25777): com.android.internal.os.ZygoteInit.main(ZygoteInit.java:702)

multiViewGroup的draw方法被调用之前,先调用了MultiViewGrou.computeScroll方法,上面的computeScroll方法中调用mScroller.computeScrollOffset()方法更新了MultiViewGroup的mScrollX和mScrollY,然后在调用TestTextView的draw方法的时候会使用这个更新后的mScrollX和mScrollY来绘制新的MultiViewGroup。
上面就是一次“滑动中间位置”一帧计算和绘制的全过程,然后看是如何继续下一帧的。在computeScroll方法里面调用了invalidate方法(实际上并不是这个起了作用,下面分析问题2的时候会讲到)进行又一次的重绘,其实在这一次重绘事件请求发出前,上次帧的绘制还没有开始,因为上面的MultiViewGrou.computeScroll方法是在MultiViewGrou.draw方法前就调用了,看上去是有点乱,但是系统会帮助我们进行有条不紊的绘制。
在MultiViewGrou.computeScroll方法里面的invalidate方法会再一次触发整个绘制流程,从而又出现了上面的绘制步骤,绘制步骤中会调用MultiViewGrou.computeScroll方法,然后进行了scrollTo滑动,并且invalidate又触发了下一次的绘制流程,直到整个滑动动画结束的时候,MultiViewGrou.computeScroll方法里面的mScroller.computeScrollOffset()方法会返回false,从而不再出现invalidate调用,结束了整个的滑动动画。

问题2:根据最前面的scrollTo的注释,可以看到scrollTo会主动去调用invalidate()方法,但是为什么后面又要调用一次invalidate方法,网上的demo里面都说不调用会有误差,根据实际的测试,确实会出现误差,明明已经调用了invalidate,然后重复一次invalidate就没有误差了,真是奇怪,原因到底是什么?

上面问题1的流程网上基本上都说清楚了,但是问题2这个问题大家都不在乎,但是到底是为什么呢?答案就需要看源码了,先看View.scrollTo代码:

    /**
     * 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.   这个函数会导致onScrollChanged的调用,同时会invalidate这个View
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(intx, inty) {
        if (mScrollX != x || mScrollY != y) {
            intoldX = mScrollX;
            intoldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

很明显这里的postInvalidateOnAnimation方法比较惹人注意,但是这个方法的调用是有条件的,必须要awakenScrollBars方法返回false才会调用,看一下awakenScrollBars方法:

protected boolean awakenScrollBars() {
        returnmScrollCache != null &&   //这两个条件同时为true的时候才会返回true
                awakenScrollBars(mScrollCache.scrollBarDefaultDelayBeforeFade, true);
    }
    protected boolean awakenScrollBars(intstartDelay, booleaninvalidate) {
        final ScrollabilityCache scrollCache = mScrollCache; 
        if (scrollCache == null || !scrollCache.fadeScrollBars) {
            returnfalse;
        } 
        if (scrollCache.scrollBar == null) {
            scrollCache.scrollBar = new ScrollBarDrawable();
        } 
        if (isHorizontalScrollBarEnabled() || isVerticalScrollBarEnabled()) { //默认情况下没有调用setVerticalScrollBarEnabled或者setHorizontalScrollBarEnabled方法的时候,这两个都是false
            if (invalidate) {   //上面给的参数是true
                // Invalidate to show the scrollbars
                postInvalidateOnAnimation();
            }
            if (scrollCache.state == ScrollabilityCache.OFF) {
                // FIXME: this is copied from WindowManagerService.
                // We should get this value from the system when it
                // is possible to do so.
                final int KEY_REPEAT_FIRST_DELAY = 750;
                startDelay = Math.max(KEY_REPEAT_FIRST_DELAY, startDelay);
            }
            // Tell mScrollCache when we should start fading. This may
            // extend the fade start time if one was already scheduled
            longfadeStartTime = AnimationUtils.currentAnimationTimeMillis() + startDelay;
            scrollCache.fadeStartTime = fadeStartTime;
            scrollCache.state = ScrollabilityCache.ON; 
            // Schedule our fader to run, unscheduling any old ones first
            if (mAttachInfo != null) {
                mAttachInfo.mHandler.removeCallbacks(scrollCache);
                mAttachInfo.mHandler.postAtTime(scrollCache, fadeStartTime);
            }
            return true;
        } 
        return false;
    }

根据上面的分析,这里awakenScrollBars()的调用会返回false,因此会调用postInvalidateOnAnimation方法,即使设置了setVerticalScrollBarEnabled或者setHorizontalScrollBarEnabled,awakenScrollBars返回了true,那么在调用awakenScrollBars方法的时候invalidate参数是true,还是会调用postInvalidateOnAnimation方法。既然scrollTo一定会调用postInvalidateOnAnimation方法,看一下这个方法:

   /**
     * 

Cause an invalidate to happen on the next animation time step, typically the * next display frame.

*

This method can be invoked from outside of the UI thread * only when this View is attached to a window.

* 触发invalidate在下一个动画时间的时候发生,这个函数还可以在UI线程之外调用,如果这个View依附到了一个window上面 * @see #invalidate() */
// View.java public void postInvalidateOnAnimation() { // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this); } } // ViewRootImpl.java public void dispatchInvalidateOnAnimation(View view) { mInvalidateOnAnimationRunnable.addView(view); } // ViewRootImpl.java final class InvalidateOnAnimationRunnable implementsRunnable { private boolean mPosted; private final ArrayList mViews = newArrayList(); private final ArrayList mViewRects = newArrayList(); private View[] mTempViews; private AttachInfo.InvalidateInfo[] mTempViewRects; public void addView(View view) { synchronized (this) { mViews.add(view); postIfNeededLocked(); } } @Override public void run() { final int viewCount; final int viewRectCount; synchronized (this) { mPosted = false; viewCount = mViews.size(); //当前需要处理的View的数量 if (viewCount != 0) { mTempViews = mViews.toArray(mTempViews != null ? mTempViews : new View[viewCount]); //将需要处理的View拷贝到数组mTempViews mViews.clear(); } viewRectCount = mViewRects.size(); if (viewRectCount != 0) { mTempViewRects = mViewRects.toArray(mTempViewRects != null ? mTempViewRects : new AttachInfo.InvalidateInfo[viewRectCount]); mViewRects.clear(); } } for (inti = 0; i < viewCount; i++) { mTempViews[i].invalidate(); //依次调用每个View的invalidate方法 mTempViews[i] = null; } for (inti = 0; i < viewRectCount; i++) { final View.AttachInfo.InvalidateInfo info = mTempViewRects[i]; info.target.invalidate(info.left, info.top, info.right, info.bottom); info.recycle(); } } privatevoidpostIfNeededLocked() { if (!mPosted) { mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null); mPosted = true; } } }

可以看到上面的postInvalidateOnAnimation最终也调用了mChoreographer.postCallback方法,并且将这个this(是一个Runnable对象)作为参数传进了Choreographer的处理链表,这里面具体的原理要再去细看,postCallback这个方法就会去请求垂直同步信号,并且在垂直同步信号到来的时候回调这里的InvalidateOnAnimationRunnable.run方法, 这个方法会导致之前加到mViews中的每一个View的invalidate方法被调用,而且这里的时间顺序要仔细分析一下:

(1)上面的代码中忽略了View.scrollTo的实际作用,整个滑动动画的顺序是:
Scroller.startScroll(触发Scroller开始计算)–>View.invalidate(触发绘制流程)–>…–>View.draw(View的绘制流程)–>View.computeScroll(计算View的滑动位置)

(2)但是实际上去掉computeScroll中的View.invalidate方法后形成的顺序是:
Scroller.startScroll(触发Scroller开始计算)–>View.invalidate(触发绘制流程)–>…–>View.draw(View的绘制流程)–>View.computeScroll(计算View的滑动位置)
–>View.scrollTo(请求在下一次的垂直同步信号来的时候进行View.invalidate)–>View.onDraw(当前这一次绘制实际的View)

(3)上面(2)中整理后按照时间的详细顺序

根据上面的分析,需要将由invalidate触发的第1帧和后面的中间帧的流程分开:
第1帧:
*Scroller.startScroll(触发Scroller开始计算)–>View.invalidate(触发重绘请求)–>ViewRootImpl.scheduleTraversals(进入垂直同步信号请求流程)–>
–>Choreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL,加入绘制事件回调)…(等待同步信号)..
–>ViewRootImpl.performTraversals(响应垂直同步信号)–>ViewRootImpl.performDraw(分发绘制事件)–>View.draw(View的绘制流程)
–>View.computeScroll(计算View的滑动位置)–>View.scrollTo(设置本次的滑动位置,View.postInvalidateOnAnimation请求下一次的垂直同步信号)–>View.onDraw(当前这一次绘制实际的View)
–>…(等待scrollTo请求的下一个同步信号)..–>*

后续帧(比较复杂):
*Choreographer. FrameDisplayEventReceiver.onVsync(获取同步信号,发送异步消息)–>Choreographer.FrameDisplayEventReceiver.run–>Choreographer.doFrame(处理同步信号回调事件)
–>Choreographer.doCallbacks(Choreographer.CALLBACK_ANIMATION,先处理动画回调)–>ViewRootImpl.InvalidateOnAnimationRunnable.run
–>View.invalidate(触发重绘请求)–>ViewRootImpl.scheduleTraversals(进入垂直同步信号请求流程)–>Choreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL,加入绘制事件回调)
–>Choreographer.doCallbacks(Choreographer.CALLBACK_TRAVERSAL,再处理刚刚加入的绘制回调)
–>ViewRootImpl.performTraversals(不是响应下一个垂直同步信号,而是在当前的同步信号回调中立马被处理)–>ViewRootImpl.performDraw(分发绘制事件)–>View.draw(View的绘制流程)
–>View.computeScroll(计算View的滑动位置)–>View.scrollTo(设置本次的滑动位置,请求下一次的垂直同步信号)–>View.onDraw(当前这一次绘制实际的View)
–>…(等待scrollTo请求的下一个同步信号,上面invalidate也请求了,但是事件已经被当前的同步回调处理完了)..–>*
下面结合调用堆栈和代码进行分析,再看一下Scroller滑动动画的使用步骤:
创建一个Scroller对象;
调用Scroller.startScroll(起始x,起始y,滑动dx,滑动dy,滑动持续时间),invalidate请求
反复调用computeScrollOffset,然后调用View.scrollTo方法

  public class MultiViewGroup extends ViewGroup {
    public int mDurationTimeMs = 1000;
    Scroller mScroller = new Scroller();    //这里使用默认的插值器
    publicvoid startMove(){
       mScroller.startScroll(0, 0, 720, 0,mDurationTimeMs);
       invalidate();
    }
    @Override
    public void computeScroll() {         
       boolean needInvalidate = false;
       if (mScroller.computeScrollOffset()) {  // 如果返回true,表示动画还没有结束,会进行一次新的scrollTo滑动,如果返回true说明动画没有结束,并且下一帧滑动的位置已经在computeScrollOffset计算好
           // 产生了动画效果每次滚动一点
           scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 这里getCurrX或Y取出Scroller.computeScrollOffset计算出来的滑动位置信息,然后进行scrollTo滑动
           invalidate();  //这个是没有必要,也是没有道理的
       }
    }
}

这里不分析startMove中的invalidate调用了,这个是触发第1帧的源头,分析下面的computeScroll和里面的scrollTo流程,根据上面的流程可以看出来scrollTo里面主要做了两件事:
(1)根据Scroller.computeScrollOffset计算出来的滑动位置设置当前要绘制的帧的mScrollX和mScrollY的值;
(2)调用postInvalidateOnAnimation方法请求下一个同步信号,同步信号的回调类型为Choreographer.CALLBACK_ANIMATION,相应的回调方法是ViewRootImpl.InvalidateOnAnimationRunnable.run
刚才这一帧还没有绘制,computeScroll计算结束后,会调用View.draw,然后调用View.onDraw对当前帧进行绘制,绘制结束以后开始等待下一个垂直同步信号的到来,下一个同步信号是由postInvalidateOnAnimation请求,对应的回调流程看下面的堆栈:

I/System.out(10309): -----------------invalidate stack---------------------
I/System.out(10309): dalvik.system.VMStack.getThreadStackTrace(Native Method)
I/System.out(10309): java.lang.Thread.getStackTrace(Thread.java:580)
I/System.out(10309): com.qin.scrollerview.LogPrinter.printStack(LogPrinter.java:30)
I/System.out(10309): com.qin.scrollerview.TestTextView.invalidate(TestTextView.java:135)
I/System.out(10309): android.view.ViewRootImpl$InvalidateOnAnimationRunnable.run(ViewRootImpl.java:5930)
I/System.out(10309): android.view.Choreographer$CallbackRecord.run(Choreographer.java:818)
I/System.out(10309): android.view.Choreographer.doCallbacks(Choreographer.java:617)
I/System.out(10309): android.view.Choreographer.doFrame(Choreographer.java:582)
I/System.out(10309): android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:804)
I/System.out(10309): android.os.Handler.handleCallback(Handler.java:739)
I/System.out(10309): android.os.Handler.dispatchMessage(Handler.java:95)
I/System.out(10309): android.os.Looper.loop(Looper.java:135)
I/System.out(10309): android.app.ActivityThread.main(ActivityThread.java:5249)
I/System.out(10309): java.lang.reflect.Method.invoke(Native Method)
I/System.out(10309): java.lang.reflect.Method.invoke(Method.java:372)
I/System.out(10309): com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:907)
I/System.out(10309): com.android.internal.os.ZygoteInit.main(ZygoteInit.java:702)

这个堆栈流程描述的很清楚,首先这个垂直同步的消息是发送到Choreographer.mDisplayEventReceiver.onVsync方法里面的,FrameDisplayEventReceiver是Choreographer的一个内部类,看一下这里面的onVsync方法:

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            //......省略一些代码 
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

向Choreographer.mHandler中发送一条异步消息,注意这里的msg.setAsynchronous(true)是很有讲究的,在绘制Choreographer.CALLBACK_TRAVERSAL被加入的时候就会阻塞非异步消息的处理,这里只有异步消息才能被处理,看这里发送的是一个callback,这个callback会去回调Choreographer.mDisplayEventReceiver.run方法:

        @Override
        publicvoid run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }

调用到了Choreographer.doFrame:

```
    void doFrame(long frameTimeNanos, int frame) {
        finallong startNanos;
        synchronized (mLock) {
            //判断是否存在跳帧
            startNanos = System.nanoTime();
            finallong jitterNanos = startNanos - frameTimeNanos;
            if (jitterNanos >= mFrameIntervalNanos) {
                finallong skippedFrames = jitterNanos / mFrameIntervalNanos;
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                }
                finallong lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                frameTimeNanos = startNanos - lastFrameOffset;
            }
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
        } 
        //进行Choreographer的垂直同步信号的回调,按照Choreographer.CALLBACK_INPUT、Choreographer.CALLBACK_ANIMATION到Choreographer.CALLBACK_TRAVERSAL的顺序分别处理
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
    }

调用到了Choreographer.doCallbacks方法:

    void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            //取出mCallbackQueues[callbackType]链表中所有时间小于等于now的所有的请求,形成一个callbacks链表 
            finallong now = SystemClock.uptimeMillis();
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now);
            if (callbacks == null) {
                return;
            }
            mCallbacksRunning = true;
        }
        try {
            //一次性进行处理上面取出的callbacks链表的所有的回调
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                c.run(frameTimeNanos);     //调用请求的时候注册的回调进行处理
            }
        } finally {
            synchronized (mLock) {
                mCallbacksRunning = false;
                do {
                    //回收上面的callbacks链表中的元素 
                    final CallbackRecord next = callbacks.next;
                    recycleCallbackLocked(callbacks);
                    callbacks = next;
                } while (callbacks != null);
            }
        }
    }

上面列出了堆栈中涉及到的应用中的所有的调用函数,最重要的一点是:
//进行Choreographer的垂直同步信号的回调,按照Choreographer.CALLBACK_INPUT、Choreographer.CALLBACK_ANIMATION到Choreographer.CALLBACK_TRAVERSAL的顺序分别处理

        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

这里会分别对几种类型的注册回调进行分别处理,并且是存在先后顺序的,先到达InvalidateOnAnimationRunnable.run方法,根据上面给出的InvalidateOnAnimationRunnable.run代码会发现里面调用了View.invalidate方法,这个方法最终会到达ViewRootImpl.invalidateChildInParent方法,然后转向ViewRootImpl.scheduleTraversals方法:

    void scheduleTraversals() {
        if (!mTraversalScheduled) {         //在本轮的同步信号的响应过程中,Choreographer.CALLBACK_TRAVERSAL是否被加入的标记,可以看出来在一个同步信号周期内只能加入一个这样的消息
            mTraversalScheduled = true;          
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier(); //同步信号的阻塞消息
            mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);  //请求下一个同步信号,并将mTraversalRunnable作为回调
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
        }
    }

上面的View.invalidate处理结束以后,达到如下效果:
(1)请求下一个同步信号;
(2)在mChoreographer.CALLBACK_TRAVERSAL处理链表中加入一条处理回调
InvalidateOnAnimationRunnable.run处理结束一个就会继续处理:
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
注意上面在InvalidateOnAnimationRunnable.run处理过程中调用View.invalidate向Choreographer.CALLBACK_TRAVERSAL对应的链表中加入一条处理记录,所以这里立马就会进行处理,处理的内容就是ViewRootImpl.mTraversalRunnable里面的run方法:

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
  final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

看一下里面的 ViewRootImpl.doTraversal方法:

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;   //设置标记为false,Choreographer.CALLBACK_TRAVERSAL又可以被加入了
            mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);   //移除刚才在scheduleTraversals中postSyncBarrier的同步阻塞信号
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
            try {
                performTraversals();   //进入绘制的流程
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

绘制的时候又会调用到View.computeScroll方法中的scrollTo请求下一次的垂直同步信号的回调(其实上面的View.invalidate已经发出了垂直同步信号的请求,但是它的回调已经被处理掉了),然后形成一个循环直到View.computeScroll方法中不再发出垂直同步信号的请求为止就结束了。
下面是一个整个过程中前几帧的调用过程,并且以上一次同步信号的时间为时间0点,理想情况下如果一直请求同步信号,那么每16ms会收到一次同步请求,第一次请求同步信号的时间是随机的,会在下一个垂直同步信号到的时间点得到响应。

误差分析:大家都说会存在误差,然后手动调用invalidate,但是原因是什么呢?
原因就在View.scrollTo方法里面,看一下代码:

    public void scrollTo(intx, inty) {
        if (mScrollX != x || mScrollY != y) {
            intoldX = mScrollX;
            intoldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

这里面传入的x y就是当前帧需要滑动到的位置,原因就在于这里mScrollX == x && mScrollY == y导致的,一旦相等就不会调用postInvalidateOnAnimation请求下一个同步信号,这样整个滑动驱动就没有了,也就不会继续了。为什么会出现相等的情况呢,看一下Scroller里面默认的插值器:

    public boolean computeScrollOffset() {
        if (mFinished) {
            returnfalse;
        }
        inttimePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);   
        if (timePassed < mDuration) {
            switch (mMode) {
            caseSCROLL_MODE:
                finalfloatx = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);    //原因就在这里,这里经过计算以后进行了取整,这就导致出现两次计算出的结果是一样的
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            }
        }
        return true;    
    }

那么为什么反复调用invalidate就可以到达效果呢?
这就要看整个滑动的驱动过程了:
startScroll–invalidate–computeScroll–scrollTo–invalidate–onDraw–computeScroll–scrollTo–invalidate–onDraw–computeScroll–scrollTo–invalidate–onDraw
假设出现mScrollX == x && mScrollY == y相等的情况,导致scrollTo没有办法执行:
startScroll–invalidate–computeScroll–scrollTo–invalidate–onDraw–computeScroll–invalidate–onDraw–computeScroll–invalidate–onDraw–computeScroll–scrollTo–invalidate–onDraw
看中间红字部分那一段,相等的时候scrollTo没有办法执行,就会通过invalidate反复驱动computeScroll进行计算,直到满足mScrollX != x || mScrollY != y就可以继续滑动了。
然后我们分析一下这个突兀的invalidate的实际作用:

(1)scrollTo能够执行时,View.invalidate在ViewRootImpl.InvalidateOnAnimationRunnable.run会在下一个垂直同步信号来的时候调用View.invalidate,再结合对上面的Choreographer的分析,可以看出来在上一个Choreographer.CALLBACK_TRAVERSAL记录被处理前,是没有办法加入新的Choreographer.CALLBACK_TRAVERSAL记录请求下一个垂直同步信号,下一个垂直信号来的时候会先处理Choreographer.CALLBACK_ANIMATION记录,系统尝试调用View.invalidate插入的Choreographer.CALLBACK_TRAVERSAL记录,但是之前的Choreographer.CALLBACK_TRAVERSAL记录尚未处理,因此系统加入的这个垂直同步信号会失败;

(2)scrollTo不能够执行时,就像上面分析的那样scrollTo中的postInvalidateOnAnimation不会被执行,滑动过程没有了驱动从而停止,这时候invalidate会一直推动整个过程反复computeScroll直到scrollTo能够执行为止
可以通过自己定义比较好的插值器来减少这样的概率,分析出问题就可以改进了,实际上根据设计来看invalidate是没有任何必要的,但是去掉以后,如何做到没有误差呢?
分析出原因就可以解决了,我们可以在scrollTo没有办法执行的时候才去invalidate推动整个滑动过程:

 public void computeScroll() {
  if (mScroller.computeScrollOffset()) {     
   if (mScroller.getCurrX() == getScrollX() && mScroller.getCurrY() == getScrollY()) {
        invalidate();
        return;
    }else{
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    }
  }
 }

//暂时写到这里吧,还有有关自定义View的时候onMeasure和onLayout来配合的过程,网上的demo都很多,还有结合VelocityTracker计算滑动速度来滑动的有时间再另外补充。

你可能感兴趣的:(Android)