Android应用开发Scroller详解及源码浅析

1 背景

大家都知道Android View提供了scrollTo()与scrollBy()方法来供我们进行View的滚动,但是有个问题就是他的滚动很蛋疼,疼在是瞬时挪动到指定位置的,这种对于追求用户体验的今天来说简直是硬伤啊;为了解决这个问题Google给我们提供了一个牛叉的工具类Scroller,下面我们就深入浅出的来开战这一工具类,将其玩爆,以便日后自定义控件时如鱼得水。

Scroller可以让我们的滚动变得十分优雅,可以瞬间提升我们自定义控件的逼格,但是了解该篇之前请先吃饱《Android应用坐标系统全面详解》一文,因为他们关系十分密切;当然喏,当你看完本文如果想看看Google自己对Scroller高端的使用则还可以继续看看《Android应用ViewDragHelper详解及部分源码浅析》一文,哈哈。

PS:要过年了,公司一片动荡。。。。。。

这里写图片描述

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】

2 Scroller基础实例

和以前博文一样,开始源码分析前先给出一个使用的基本例子作为引导,否则都不知道自己在看啥。这里我们给出一个比较常见的东东—–侧滑拉出收起(类似QQ List列表Item的效果)。如下是控件Demo效果(请原谅我Ubuntu gif):

Android应用开发Scroller详解及源码浅析_第1张图片

示例源码点我下载,不过只是Demo给出思路,细节没有处理,也没有进行完善,只是作为Scroller的Demo。下面是该控件实现的核心代码:

public class HorizontalFlingLayout extends LinearLayout {
    private Scroller mScroller;

    private View mLeftView;
    private View mRightView;

    private float mInitX, mInitY;
    private float mOffsetX, mOffsetY;

    public HorizontalFlingLayout(Context context) {
        this(context, null);
    }

    public HorizontalFlingLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HorizontalFlingLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        this.setOrientation(LinearLayout.HORIZONTAL);

        mScroller = new Scroller(getContext(), null, true);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        if (getChildCount() != 2) {
            throw new RuntimeException("Only need two child view! Please check you xml file!");
        }

        mLeftView = getChildAt(0);
        mRightView = getChildAt(1);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mInitX = ev.getX();
                mInitY = ev.getY();
                super.dispatchTouchEvent(ev);
                return true;
            case MotionEvent.ACTION_MOVE:
                //>0为手势向右下
                mOffsetX = ev.getX() - mInitX;
                mOffsetY = ev.getY() - mInitY;
                //横向手势跟随移动
                if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > ViewConfiguration.getTouchSlop()) {
                    int offset = (int) -mOffsetX;
                    if (getScrollX() + offset > mRightView.getWidth() || getScrollX() + offset < 0) {
                        return true;
                    }
                    this.scrollBy(offset, 0);
                    mInitX = ev.getX();
                    mInitY = ev.getY();
                    return true;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                //松手时刻滑动
                int offset = ((getScrollX() / (float)mRightView.getWidth()) > 0.5) ? mRightView.getWidth() : 0;
//                this.scrollTo(offset, 0);
                mScroller.startScroll(this.getScrollX(), this.getScrollY(), offset-this.getScrollX(), 0);
                invalidate();
                mInitX = 0;
                mInitY = 0;
                mOffsetX = 0;
                mOffsetY = 0;
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
}

简单吧,使用Scroller就能这么优雅的滑动,不解释,简单的Demo,哈哈;有了这个基本映像我们直接高速——源码探测,搞清源码基本原理流程就能用的顺手喽。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】

3 Scroller源码浅析

通过上面实例我们可以发现在自定义View的过程中使用Scroller的流程如下图所示:

Android应用开发Scroller详解及源码浅析_第2张图片

既然有了这么明确的流程图,那我们下面就来依据这个流程简单分析下Scroller的源码。可以发现Scroller这类的代码不多哇,确实是一个工具类,哈哈,我们先看下构造方法:

public Scroller(Context context) {
    this(context, null);
}

public Scroller(Context context, Interpolator interpolator) {
    this(context, interpolator,
        context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}

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
}

可以看见,构造方法没啥特殊的,只是一些基础的设置,唯一要重点关注可能自定义的也就动画插值器那个参数了,默认是ViscousFluidInterpolator的(不懂动画插值器的请看《Android应用开发之所有动画使用详解》),我们可以自定义修改。两参构造方法中其实也就是对第三个参数做了HONEYCOMB兼容性处理,三参是所有构造方法最终调运的方法,其实也就是初始化了一些变量而已,没啥重要的。

下面我们看看与Scroller相关的startScroll()和fling()方法,源码如下:

//在我们想要滚动的地方调运,准备开始滚动,默认滚动时间为DEFAULT_DURATION
public void startScroll(int startX, int startY, int dx, int dy) {
    startScroll(startX, startY, dx, dy, DEFAULT_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;
}

//在快速滑动松开的基础上开始惯性滚动,滚动距离取决于fling的初速度
public void fling(int startX, int startY, int velocityX, int velocityY,
    int minX, int maxX, int minY, int maxY) {
    ......
    mMode = FLING_MODE;
    mFinished = false;
    ......
    mStartX = startX;
    mStartY = startY;
    ......
    mDistance = (int) (totalDistance * Math.signum(velocity));

    mMinX = minX;
    mMaxX = maxX;
    mMinY = minY;
    mMaxY = maxY;
    ......
    mFinalY = Math.min(mFinalY, mMaxY);
    mFinalY = Math.max(mFinalY, mMinY);
}

可以看见,上面这几个美其名曰滑动的Scroller方法其实都只是一个幌子,没有进行滑动,而是初始化了一堆成员变量;譬如滚动模式、开始时间、持续时间等,也就是说他们都只是工具方法而已,实质的滑动其实是需要我们在他后面手动调运View的invalidate()进行刷新,然后在View进行刷新时又会调运自己的View.computeScroll()方法(不了解View绘制的请看《Android应用层View绘制流程与源码分析》一文),在View.computeScroll()方法中进行Scroller.computeScrollOffset()判断与触发View的滑动方法。

既然这样那我们粗略给出View的绘制流程,详细的请看《Android应用层View绘制流程与源码分析》一文。当我们调运invalidate()会触发View的如下方法:

public void draw(Canvas canvas) {
    ......
    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */
    ......
    // Step 4, draw the children
    dispatchDraw(canvas);
    ......
}

可以发现,View的draw()方法被触发时总共会进行6步,最重要的一步我们看第四步,下面是第四步dispatchDraw()方法源码:

protected void dispatchDraw(Canvas canvas) {}

可以看见,View的该方法为空方法,那我们看下他子类ViewGroup的该方法,如下:

protected void dispatchDraw(Canvas canvas) {
    ......
    for (int i = 0; i < childrenCount; i++) {
        ......
        more |= drawChild(canvas, child, drawingTime);
        ......
    }
    ......
}

可以发现,ViewGroup的dispatchDraw()方法实质又跑到了drawChild()方法,如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

额额,实质又是child的另一个draw()方法而已,我们回到View去看下这个方法,如下:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ......
    if (!drawingWithRenderNode) {
        computeScroll();
        sx = mScrollX;
        sy = mScrollY;
    }
    ......
}

额额,这就解释了为何View调运invalidate()就会触发computeScroll()方法了。而ViewGroup最终调运scrollTo()方法都只能滚动内部子View的问题其实是因为ViewGroup它本身并没有任何可画的东西,它是一个透明的控件,所以一般不会触发onDraw()方法,但是当你给他设置背景等就会调用onDraw方法了,可是走的是绘制背景流程。

View相关的扯完了,下面我们来看看Scroller的computeScrollOffset()方法,下面我们简单分析这个方法,如下:

//判断滚动是否还在继续,true继续,false结束
public boolean computeScrollOffset() {
    //mFinished为true表示已经完成了滑动,直接返回为false
    if (mFinished) {
        return false;
    }
    //mStartTime为开始时的时间戳,timePassed就是当前滑动持续时间
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    //mDuration为我们设置的持续时间,当当前已滑动耗时timePassed小于总设置持续时间时才进入if
    if (timePassed < mDuration) {
        //mMode有两中,如果调运startScroll()则为SCROLL_MODE模式,调运fling()则为FLING_MODE模式
        switch (mMode) {
        case SCROLL_MODE:
        //根据Interpolator插值器计算在该时间段里移动的距离赋值给mCurrX和mCurrY
        final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
        mCurrX = mStartX + Math.round(x * mDeltaX);
        mCurrY = mStartY + Math.round(x * mDeltaY);
        break;
        case FLING_MODE:
        //各种数学运算获取mCurrY、mCurrX,实质类似上面SCROLL_MODE,只是这里时惯性的
        ......
        // 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 {
        //认为滑动结束,mFinished置位true,标记结束,下一次再触发该方法时一进来就判断返回false了
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}

可以看见该方法的作用其实就是实时计算滚动的偏移量(也是一个工具方法),同时判断滚动是否结束(true代表没结束,false代表结束)。

到此整个Scroller就分析完了,剩下的全是各种getXXX、setXXX方法就没啥意思了。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】

4 Scroller总结

基于上面的例子和分析我们进行如下总结:

public class Scroller  {
    ......
    public Scroller(Context context) {}
    public Scroller(Context context, Interpolator interpolator) {}
    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {}
    //设置滚动持续时间
    public final void setFriction(float friction) {}
    //返回滚动是否结束
    public final boolean isFinished() {}
    //强制终止滚动
    public final void forceFinished(boolean finished) {}
        //返回滚动持续时间
    public final int getDuration() {}
    //返回当前滚动的偏移量
    public final int getCurrX() {}
    public final int getCurrY() {}
    //返回当前的速度
    public float getCurrVelocity() {}
    //返回滚动起始点偏移量
    public final int getStartX() {}
    public final int getStartY() {}
        //返回滚动结束偏移量
    public final int getFinalX() {}
    public final int getFinalY() {}
    //实时调用该方法获取坐标及判断滑动是否结束,返回true动画没结束
    public boolean computeScrollOffset() {}
    //滑动到指定位置
    public void startScroll(int startX, int startY, int dx, int dy) {}
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {}
    //快速滑动松开手势惯性滑动
    public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) {}
    //终止动画,滚到最终的x、y位置
    public void abortAnimation() {}
    //延长滚动的时间
    public void extendDuration(int extend) {}
    //返回滚动开始经过的时间
    public int timePassed() {}
    //设置终止时偏移量
    public void setFinalX(int newX) {}
    public void setFinalY(int newY) {}
}

至此Scroller就结束了,相关问题可以自行脑补,相信有了该篇的帮助你的自定义之路又暂时明朗了一段。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】

Android应用开发Scroller详解及源码浅析_第3张图片

你可能感兴趣的:(Android应用框架浅析)