Scroller源码解析

来源:互联网 发布:vue.js 日期格式化 编辑:程序博客网 时间:2024/06/05 11:19

前言:关于Scroller其实在上面一篇博客有提过,大致的说了下使用流程,没有深入细节研究,这篇博客将会带你从源码的角度结合案例来深入了解它。

Scroller是Android开发中用来辅助处理弹性滑动的一个类,啥叫弹性滑动呢?让你的View在滑动过程中有个平缓的过程,而不是生硬的滑过去。增加用户体验效果,这就是Scroller。从本身来说没有什么意义,单独也不能使用,需要配合View的相关方法才能发挥其作用,Android之中像可以滑动的ListView,GridView,ViewPager等其实底层都使用了Scroller,由此可见Scroller的作用还是很大的,那么了解Scroller的底层原理是一个高级Android开发工程师所必备掌握的,了解其底层原理,我们也可以写出一个简易版的ViewPager,说的滑动大家应该明白在Android中所有的View都是可以滑动的,因为所有的View都有scrollTo和scrollBy两个用来滑动的方法,关于这两个方法的使用和区别这里就不在多说,可参考前一篇博客,scrollTo是相对于目标位置的绝对滑动,scrollBy是相对于当前位置的相对滑动,scrollBy底层其实就调用了scrollTo方法。

下面我们首先来看段代码,使用弹性滑动,将一个View滑动到指定的位置。

private Scroller mScroller = new Scroller(mContext);//将一个View缓慢的滚动到指定位置private void smoothScrollTo(int x , int y){    int scrollX = getScrollX();    int deltaX = x - scrollX;    //在一秒内将View滑动了deltaX个像素    mScroller.startScroll(scrollX, 0, deltaX, 0,1000);    invalidate();}@Overridepublic void computeScroll() {    if(mScroller.computeScrollOffset()){        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());        postInvalidate();    }}

是不是发现很简单,其实关于Scroller的使用流程是很固定的,具体会在下面进行分析,上面是一个简单的操作,接下来我们开始分析工作流程。
首先当我们实例化一个Scroller的时候,它内部什么也没做,只是完成了一些变量的初始化,源码如下:

/** * Create a Scroller with the default duration and interpolator. */public Scroller(Context context) {    this(context, null);}

接着当我们调用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;}

观察代码可以发现还是什么都没做,只是记录这些变量。关于这些变量的含义需要给大家介绍下:
startX和startY代表滑动开始时X轴的坐标和Y轴的坐标,也就是滑动的起点。dX和dY代表X轴和Y轴要滑动的距离(需要注意,这里如果往右滑为负,往左滑为正,往上滑为正,往下滑为负)。
duration代表滑动所持续的时间,单位为毫秒。到这里大家应该明白光靠调用startScroll这个方法是根本不能让View进行滑动的,那么View到底是怎么滑动的呢?答案就是下面的invalidate方法,没错,就是它,那么问题来了它又是怎么让View进行滑动的呢,我们知道调用invalidate方法会让View进行重绘的,你已经知道View进行重绘的时候必定会调用View的draw方法,draw方法最后会走到dispatchDraw方法,源码如下:

protected void dispatchDraw(Canvas canvas) {    .    .    .    .    if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {            for (int i = 0; i < count; i++) {                final View child = children[i];                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {                    more |= drawChild(canvas, child, drawingTime);                }            }        } else {            for (int i = 0; i < count; i++) {                final View child = children[getChildDrawingOrder(count, i)];                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {                    more |= drawChild(canvas, child, drawingTime);                }            }        } }

drawChild方法如下:

  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {    .    .    .    .    if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&            (child.mPrivateFlags & DRAW_ANIMATION) == 0) {        return more;    }    child.computeScroll();    final int sx = child.mScrollX;    final int sy = child.mScrollY; }

可以发现只要我们进行View的invalidate重绘,方法最后必定会都到View的computeScroll方法,还记得我们在上面那段代码处理View滑动时候覆写的computeScroll方法吧,到这里你应该明白这是由于这个computeScroll方法View才能实现弹性滑动,主要流程是这样的,我再完整叙述一遍:当我们调用View的invalidate进行视图重绘的时候会调用View的draw方法,在draw方法底层会调用dispatchDraw方法,而又在dispatchDraw方法底层最终调用computeScroll方法,在这个方法中又会去向Scroller对象获取当前的scrollX和scrollY,然后通过scrollTo方法来进行滑动,接着又调用postInvalidate方法来进行二次重绘,然后又继续向Scroller对象获取当前的scrollX和scrollY,并通过scrollTo方法来滑动到指定位置,如此反复,直至整个平滑过程结束。

最后我们来看下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);            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;}

代码不多,我们来分析下,首先看第二句代码

if (mFinished) {        return false;    }

如果mFinished为真则直接return false结束,下面不在执行。如果mFinished为假则跳过执行下面的代码,可以发现mFinished默认是false的,还记得在我们进行滑动的时候一开始调用的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;}

很显然到这里你应该明白了,mFinished 默认为false,代码继续往下执行,在第六行我们可以看到算出来一个时间差,单位是毫秒,AnimationUtils.currentAnimationTimeMillis()获取的是当前的时间(注意这里的时间指的是从开机到现在的时间),mStartTime 为程序刚刚调用startScroll方法的时候的时间毫秒值,接着看第八行代码,到这里你应该明白在你不断的进行View视图重绘的情况下,这里的timePassed会随着时间的流逝来渐渐增加一直到我们动画持续的时间,接着往下,mMode其实就是SCROLL_MODE,然后下面代码大致的意思就是根据时间的流逝算出来一个百分比,接着下面两句代码:

mCurrX = mStartX + Math.round(x * mDeltaX);mCurrY = mStartY + Math.round(x * mDeltaY);

不断的给mCurrX和mCurrY进行赋值,到这里你是不是焕然大悟?mCurrX和mCurrY正是我们前面通过Scroller获取的getmCurrX()和getmCurrY(),mStartX和mStartY是我们刚开始时候的起始位置,每次小幅度的加上移动的小距离,最终又通过scrollTo来滑动到此位置,n此小幅度的滑动最终就形成了平滑的效果,即弹性滑动。
可以发现只要动画没有结束那么就一直会返回true,到动画结束了的时候会执行:

mCurrX = mFinalX;mCurrY = mFinalY;mFinished = true;

也就是会给mFinished = true;最后在上面直接返回了false,代表动画结束。也可以通过Scroller.isFinished()返回true来判断动画滑动结束。

至此,关于Scroller你所了解的一切就结束了。如有疑问可以在下方留言。接下来 我们通过一个案例来熟练下Scroller,我选用了仿微信右滑消失的功能来讲解下,网上代码很多,主要还是理解Scroller用法。新建SwipeBackLayout继承自FrameLayout代码如下:

/** *  * @author xyy 仿微信右滑消失 *  */public class SwipeBackLayout extends FrameLayout {    private static final String TAG = SwipeBackLayout.class.getSimpleName();    /** 当前的View */    private View mContentView;    /** 滑动的最小距离 */    private int mTouchSlop;    /** 按下时相对屏幕的X轴坐标 */    private int downX;    /** 按下时相对屏幕的Y轴坐标 */    private int downY;    /** 用来记录X轴坐标的临时变量 */    private int tempX;    /** 处理弹性滑动的辅助类 */    private Scroller mScroller;    /** 当前布局的宽度 */    private int viewWidth;    /** 用来记录是否滑动 */    private boolean isSilding;    /** 用来记录当前Activity书否finish */    private boolean isFinish;    /** 给当前页面绘制的黑色的阴影效果 */    private Drawable mShadowDrawable;    /** 用来记录当前的Activity */    private Activity mActivity;    /** 存放ViewPager */    private List<ViewPager> mViewPagers = new LinkedList<ViewPager>();    public SwipeBackLayout(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();        mScroller = new Scroller(context);        mShadowDrawable = getResources().getDrawable(R.drawable.shadow_left);    }    /**     *      * @param activity     */    public void attachToActivity(Activity activity) {        mActivity = activity;        TypedArray a = activity.getTheme().obtainStyledAttributes(                new int[] { android.R.attr.windowBackground });        /** 获取typedArray数组中指定位置的资源id值 */        int background = a.getResourceId(0, 0);        a.recycle();        /** 获取Activity布局的顶层视图decorView */        ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();        /** 获取decorView的孩子 */        ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);        /** background就是上面获得的android.R.attr.windowBackground */        decorChild.setBackgroundResource(background);        /** 干掉当前decorView的孩子 */        decor.removeView(decorChild);        /** 将当前的decorChild添加到自定义的FrameLayout中 */        addView(decorChild);        /** 获取decorChild的父View */        setContentView(decorChild);        /** 将当前的SwipeBackLayout添加到顶层decorView的布局之中 */        decor.addView(this);    }    private void setContentView(View decorChild) {        mContentView = (View) decorChild.getParent();    }    /**     * 事件拦截操作     */    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        // 处理ViewPager冲突问题        ViewPager mViewPager = getTouchViewPager(mViewPagers, ev);        Log.i(TAG, "mViewPager = " + mViewPager);        // 如果当前在页面滑动的是ViewPager并且viewPager.getCurrentItem()不等于0则不拦截事件        if (mViewPager != null && mViewPager.getCurrentItem() != 0) {            return super.onInterceptTouchEvent(ev);        }        switch (ev.getAction()) {        case MotionEvent.ACTION_DOWN:            downX = tempX = (int) ev.getRawX();            downY = (int) ev.getRawY();            break;        case MotionEvent.ACTION_MOVE:            int moveX = (int) ev.getRawX();            // 按下的坐标小于screenWidth并且X轴滑动的偏移量大于mTouchSlop并且Y轴的偏移量小于mTouchSlop,则拦截事件            if (moveX - downX > mTouchSlop                    && Math.abs((int) ev.getRawY() - downY) < mTouchSlop) {                return true;            }            break;        }        return super.onInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        switch (event.getAction()) {        case MotionEvent.ACTION_MOVE:            int moveX = (int) event.getRawX();            int deltaX = tempX - moveX;            tempX = moveX;            if (moveX - downX > mTouchSlop                    && Math.abs((int) event.getRawY() - downY) < mTouchSlop) {                isSilding = true;            }            // 右滑并且按下坐标小于screenWidth            if (moveX - downX >= 0 && isSilding) {                // 让当前View开始沿着X轴滚动,Y轴不变,右滑为负                mContentView.scrollBy(deltaX, 0);            }            break;        case MotionEvent.ACTION_UP:            isSilding = false;            // 当前View在X轴滚动的距离大于当前View一半的时候此时up(右滑为负,左滑为正)            if (mContentView.getScrollX() <= -viewWidth / 2) {                isFinish = true;                // 销毁Activity                finishPage();            } else {                // 滚动到起始位置                resetPage();                isFinish = false;            }            break;        }        return true;    }    /**     * 获取SwipeBackLayout里面的ViewPager的集合     *      * @param mViewPagers     * @param parent     */    private void getAlLViewPager(List<ViewPager> mViewPagers, ViewGroup parent) {        int childCount = parent.getChildCount();        for (int i = 0; i < childCount; i++) {            View child = parent.getChildAt(i);            if (child instanceof ViewPager) {                mViewPagers.add((ViewPager) child);            } else if (child instanceof ViewGroup) {                getAlLViewPager(mViewPagers, (ViewGroup) child);            }        }    }    /**     * 返回我们touch的ViewPager     *      * @param mViewPagers     * @param ev     * @return     */    private ViewPager getTouchViewPager(List<ViewPager> mViewPagers,            MotionEvent ev) {        if (mViewPagers == null || mViewPagers.size() == 0) {            return null;        }        Rect mRect = new Rect();        for (ViewPager v : mViewPagers) {            v.getHitRect(mRect);            if (mRect.contains((int) ev.getX(), (int) ev.getY())) {                return v;            }        }        return null;    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);        if (changed) {            viewWidth = this.getWidth();            getAlLViewPager(mViewPagers, this);            Log.i(TAG, "ViewPager size = " + mViewPagers.size());        }    }    @Override    protected void dispatchDraw(Canvas canvas) {        super.dispatchDraw(canvas);        if (mShadowDrawable != null && mContentView != null) {            int left = mContentView.getLeft()                    - mShadowDrawable.getIntrinsicWidth();            int right = left + mShadowDrawable.getIntrinsicWidth();            int top = mContentView.getTop();            int bottom = mContentView.getBottom();            mShadowDrawable.setBounds(left, top, right, bottom);            mShadowDrawable.draw(canvas);        }    }    /**     * 销毁页面     */    private void finishPage() {        // delta为此时手指up的时候距离右边缘的距离        final int delta = (viewWidth + mContentView.getScrollX());        // 调用startScroll方法来设置一些滚动的参数,实现手指up时候的弹性滑动,必须在computeScroll()方法中调用scrollTo来滚动item        mScroller.startScroll(mContentView.getScrollX(), 0, -delta + 1, 0,                Math.abs(delta));        // 视图重绘        postInvalidate();    }    /**     * 滚动到起始位置     */    private void resetPage() {        int delta = mContentView.getScrollX();        mScroller.startScroll(mContentView.getScrollX(), 0, -delta, 0,                Math.abs(delta));        postInvalidate();    }    @Override    public void computeScroll() {        if (mScroller.computeScrollOffset()) {            mContentView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());            postInvalidate();            // 返回true代表滑动已经结束            if (mScroller.isFinished() && isFinish) {                mActivity.finish();            }        }    }}

效果如下:

这里写图片描述

代码很简单,这里讲解下实现过程,大家应该知道Activity布局的根视图叫做DecorView,它是一个帧布局。我们在Activity中加载的布局activity_main.xml其实是一个叫id=content的帧布局的孩子,这里简单的画了一张图,通过这张图将会很清晰布局之中的结构。

Activity页面构成图

我们自定义了一个SwipeBackLayout继承自FrameLayout,在代码的61行我们获取了关于Android系统所能识别的滑动最小距离TouchSlop,以及实例化一个Scroller。mShadowDrawable为一张9patch图,用于仿微信给Activity左侧页面绘制阴影效果。
接着在75行的attachToActivity方法中我们首先获取了根视图decorView,接着获取了它的孩子decorChild,这时候通过根视图decorView把当前decorChild这个孩子给remove掉了,然后又通过我们自定义的SwipeBackLayout帧布局将decorChild作为SwipeBackLayout孩子给添加进去了,最后整体作为孩子挂到decorView之中。
这时候的结构如下面这张图:

这里写图片描述

是不是很清晰,从图中我们可以发现这时候我们的SwipeBackLayout是包含在Activity的布局之上的。接下来就简单了,这里利用了事件分发机制,我们知道事件最先是由Activity传递到Window,接着到decorView根视图,然后层级传替到我们的SwipeBackLayout上,我们在onInterceptTouchEvent方法中判断手指的滑动操作,并且当滑动的偏移量大于我们的TouchSlope我们就认为发生了滑动操作,这时候将事件拦截掉,交由当前SwipeBackLayout的onTouchEvent方法处理,可以发现我们在onTouchEvent的move方法中再次进行判断,当手指滑动的偏移量大于TouchSlope并且右滑操作我们就调用了mContentView.scrollBy(deltaX, 0);mContentView为上方decorChild的父亲,也就是当前的SwipeBackLayout,deltaX为滑动的偏移量(右滑为负,左滑为正)这时候直接根据偏移量去做响应的滑动即可,最后在up的时候我们判断了当前滑动的距离是否大于屏幕的一半,mContentView.getScrollX() <= -viewWidth / 2,如果大于一半,则销毁Activity也就是执行finishPage()方法,代码如下:

/**     * 销毁页面     */    private void finishPage() {        // delta为此时手指up的时候距离右边缘的距离        final int delta = (viewWidth + mContentView.getScrollX());        // 调用startScroll方法来设置一些滚动的参数,实现手指up时候的弹性滑动,必须在computeScroll()方法中调用scrollTo来滚动item        mScroller.startScroll(mContentView.getScrollX(), 0, -delta + 1, 0,                Math.abs(delta));        // 视图重绘        postInvalidate();    }

如果小于屏幕的一半,此时up应该重置的开始位置,也就是执行resetPage方法,代码如下:

/**     * 滚动到起始位置     */    private void resetPage() {        int delta = mContentView.getScrollX();        mScroller.startScroll(mContentView.getScrollX(), 0, -delta, 0,                Math.abs(delta));        postInvalidate();    }

到这里,可以发现我们的Scroller终于上场了,当滑动的距离大于屏幕一半的时候我们调用了mScroller.startScroll让View开始平滑滚动,首先第一个参数为滚动开始时的X轴位置,其实也就是mContentView.getScrollX()X轴的偏移量,第二个参数为Y轴开始的位置,Y轴我们不需要变化,所以直接0即可,第三个参数为X轴滚动的距离,final int delta = (viewWidth + mContentView.getScrollX());viewWidth 为当前View的宽度加上X轴此时的偏移量(为负)此时相加得出的结果正好是要滚动的距离,最后一个参数书滚动的时候,到这里我们应该明白此时不能让View滚动的(在上面已经说过)真正滚动的是下面的postInvalidate方法,这时候会调用View的computeScroll方法,代码如下:

@Override    public void computeScroll() {        if (mScroller.computeScrollOffset()) {            mContentView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());            postInvalidate();            // 返回true代表滑动已经结束            if (mScroller.isFinished() && isFinish) {                mActivity.finish();            }        }    }

可以发现和最上面分析Scroller的时候代码几乎一致,没错,关于Scroller我们所使用的就这几个套路,代码格式很固定。在这个方法中通过mScroller.computeScrollOffset()来判断动画有没有结束,没有结束将返回true,接着继续让mContentView.scrollTo进行小幅度的移动,然后再一次的invalidate进行视图重绘,最后通过mScroller.isFinished()返回true代表动画结束,在上面我们查看源码已经知道这里不再多说,最后将Activityfinish掉。反之,则重置,滚动到起始位置,这里不再分析。到此整个Scroller案例加源码分析也就结束了。