一文读懂Scroller

作者: 九心_ | 来源:发表于2019-01-17 06:46 被阅读40次

    1.前言:

    如果你要在自定义View中添加滑动效果,那么使用Scroller可能是一个不错的选择,今天我们就来介绍一下Scroller。

    2.简单使用

    先看一下实现的效果图:

    动图.gif
    Scroller的使用方式很简单,在使用之前,如果对View.scrollTo(int x, int y)方法不了解(这里有必要申明一下,View.scrollTo(int x, int y)不是静态方法,之所以加上View.是为了和下面Scroller中的方法区分下来,下面如非特殊申明,都是该类下的普通方法),建议先看一下View.scrollTo(int x, int y),如下则是我们的使用介绍:
    1. 先创建一个自定义View类
    2. 然后继承View
    3. 创建一个方法,调用Scroller.startScroll(x,x,x,x);(这边参数省略了)和重新绘制方法View.invalidate()
    4. View.computeScroll()不断进行判断是否完成绘制,如果没有完成,还需调用滚动方法View.scrollTo(int x, int y)和重新绘制方法View.invalidate()

    最后我们还是来看一下代码:

    public class ScrollerView extends View {
        private Bitmap mBitmap;
        private Paint mPaint;
        private Scroller mScroller;
    
    
        public ScrollerView(Context context) {
            this(context,null);
        }
    
        public ScrollerView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
    
            init(context);
        }
    
        private void init(Context context) {
            mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.beauty);
            mPaint = new Paint();
            mScroller = new Scroller(context);
        }
    
        @Override
        public void draw(Canvas canvas) {
            super.draw(canvas);
    
            canvas.drawBitmap(mBitmap,100,100,mPaint);
        }
    
        public void scroll(){
            mScroller.startScroll(0,0,0,-400,10000);
            invalidate();
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
    
            if(mScroller.computeScrollOffset()){
                scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
                // 刷新绘制的界面
                invalidate();
            }
        }
     }
    

    代码还是很简单的。

    3. 从过程中分析源码

    为了更直观的学习Scroller的调用流程,我用流程图展现了出来,先看图:


    档案管理流程图.jpg
    1. 我们首先调用Scroller.startScroll(int startX, int startY, int dx, int dy, int duration)方法,其实这个方法里面只是简单的传了一下值,其他什么也没有做。
          // 这里只是简单的赋值
        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;
        }
    
    1. 然后我们需要手动调用View.invalidate()刷新我们的前面,因为我们的View不会主动刷新界面,我们都知道View.invalidate()会通知我们的界面进行重绘,这个时候View. draw(Canvas canvas)就会被调用。细心的你这个时候可能就发现了,我们的View.computeScroll()方法没有参与进来!别急,别急,我们来看一下缩减后的View. draw(Canvas canvas)的源码:
        /**
          * This method is called by ViewGroup.drawChild() to have each child  
          * view draw itself.
          * This is where the View specializes rendering behavior based on layer type,
          * and hardware acceleration.
        */
        boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
            ...
            if (!drawingWithRenderNode) {
                computeScroll();
                sx = mScrollX;
                sy = mScrollY;
            }
            ...
    
            if (!drawingWithDrawingCache) {
                if (drawingWithRenderNode) {
                    ...
                } else {
                    // Fast path for layouts with no backgrounds
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        ...
                    } else {
                        draw(canvas);
                    }
                }
            } 
            ...
        }
    

    其实在通知界面重绘的时候是先调用我们上面的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法,然后在依次调用了View.computeScroll()View. draw(Canvas canvas)方法,而View中的View.computeScroll()都是空实现,所以需要我们继承的时候自己复写方法。

    1. 下面就是讲我们的重点部分了,通常我们在使用Scroller时,都会复写View.computeScroll()方法,之后我们会调用Scroller.computeScrollOffset()来判断滑动有没有完成,如下代码:
        @Override
        public void computeScroll() {
            super.computeScroll();
            if(mScroller.computeScrollOffset()){
                scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
                // 刷新绘制的界面
                invalidate();
            }
        }
    

    整个Scroller的核心部分就是Scroller.computeScrollOffset()方法了,我们还是来看源码:

        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);
                    // 重点部分 根据Interpolator插值器计算在该时间段里移动的距离加上初始赋值赋值给mCurrX和mCurrY
                    mCurrX = mStartX + Math.round(x * mDeltaX);
                    mCurrY = mStartY + Math.round(x * mDeltaY);
                    break;
                case FLING_MODE:
                    //滑动时,抬起手执行的惯性运动,通过复杂的运算获取当前的mCurrX 、mCurrY 值。
                    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;
        }
    

    可以看到,Scroller.computeScrollOffset()并非简单的判断滑动是否完成,它还计算了当前应当滑动到的距离,最后在我们自定义的View中,通过scrollTo(mScroller.getCurrX(),mScroller.getCurrY());完成实现滑动过程,接着调用View.invalidate()重复界面刷新到绘制的过程,直到我们整个滑动过程完成。到这里,我们的整个过程就结束了。

    4. 引用

    Scroller 解析
    <<Android开发艺术探索>>

    相关文章

      网友评论

        本文标题:一文读懂Scroller

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