Android自定义View面试题

一文读懂Scroller

2019-01-17  本文已影响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开发艺术探索>>

上一篇 下一篇

猜你喜欢

热点阅读