Scroller源码解析

前言:关于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();
}
@Override
public 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 mViewPagers = new LinkedList();

    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 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 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的帧布局的孩子,这里简单的画了一张图,通过这张图将会很清晰布局之中的结构。

Scroller源码解析_第1张图片

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

Scroller源码解析_第2张图片

是不是很清晰,从图中我们可以发现这时候我们的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案例加源码分析也就结束了。

你可能感兴趣的:(Android)