码迷,mamicode.com
首页 > 移动开发 > 详细

Android Scroller源码解析

时间:2016-07-22 19:11:27      阅读:282      评论:0      收藏:0      [点我收藏+]

标签:

1. 前言

通过view本身提供的scrollTo/scrollBy方法实现滑动,其过程是瞬间的,想要实现弹性滑动的时候,需要用scroller来实现。Android里Scroller类是为了实现View平滑滚动的一个Helper类。通常在自定义的View时使用,在View中定义一个私有成员mScroller = new Scroller(context)。mScroller本身,并不会导致View的滚动,通常是用mScroller记录/计算View滚动的位置,需要重写View的computeScroll(),配合view的刷新,完成实际的滚动,后面会有详细的源码分析。

2.相关API介绍如下

mScroller.getCurrX() //获取mScroller当前水平滚动的位置  
mScroller.getCurrY() //获取mScroller当前竖直滚动的位置  
mScroller.getFinalX() //获取mScroller最终停止的水平位置  
mScroller.getFinalY() //获取mScroller最终停止的竖直位置  
mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置  
mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置  

//滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间  
mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默认完成时间250ms  
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)  

mScroller.computeScrollOffset() //返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。  

3.示例代码

一般滑动一个view,需要自定义view,然后实现smoothScrollTo(),重写computeScroll方法。代码如下:

package com.view.viewtest;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.Scroller;

public class CustomView extends LinearLayout {

    private static final String TAG = "Scroller";

    private Scroller mScroller;

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

    //调用此方法滚动到目标位置
    public void smoothScrollTo(int fx, int fy) {
        int dx = fx - mScroller.getFinalX();
        int dy = fy - mScroller.getFinalY();
        smoothScrollBy(dx, dy);
    }

    //调用此方法设置滚动的相对偏移
    public void smoothScrollBy(int dx, int dy) {

        //设置mScroller的滚动偏移量
        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
        invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
    }

    @Override
    public void computeScroll() {

        //先判断mScroller滚动是否完成
        if (mScroller.computeScrollOffset()) {

            //这里调用View的scrollTo()完成实际的滚动
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());

            //必须调用该方法,否则不一定能看到滚动效果
            postInvalidate();
        }
        super.computeScroll();
    }
}

4.源码分析

首先,看下scroller的构造方法:

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

    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
     * be in effect for apps targeting Honeycomb or newer.
     */
    public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }

只有两个构造方法,第一个只有一个Context参数,第二个构造方法中指定了Interpolator,什么Interpolator呢?中文意思插补器,了解Android动画的朋友都应该熟悉Interpolator,他指定了动画的变化率,比如说匀速变化,先加速后减速,正弦变化等等,不同的Interpolator可以做出不同的效果出来,第一个使用默认的Interpolator(viscous)

上面示例代码是scroller的典型使用方法,使用了第一种构造方法,随后我们调用自定义view的smoothScrollTo(int fx, int fy)方法的时候,方法内部会调用构造出的scroller的startScroll(int startX, int startY, int dx, int dy, int duration)方法,startScroll()源码如下所示:

//滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, 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;
    }

虽然方法名称为开始滑动,但是并没有让view滑动,很明显,内部都是一些赋值操作,没有滑动的相关代码。那为什么view可以滑动呢?看下示例代码里面,在startScroll方法下面,立即调用了invalidate()方法,而invalidate()方法会导致view的重绘。

invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果

通过上面的注释,可以知道,真正的滑动,是在computeScroll()方法里面实现的,为什么调用invalidate()才能保证computeScroll()会被调用?invalidate()方法会导致view的重绘,及调用draw()方法,我们继续看下view的draw()方法的源码:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...省略很多行
        int sx = 0;
        int sy = 0;
        if (!drawingWithRenderNode) {
            computeScroll();
            sx = mScrollX;
            sy = mScrollY;
        }

        final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
        final boolean offsetForScroll = cache == null && !drawingWithRenderNode;

        int restoreTo = -1;
        if (!drawingWithRenderNode || transformToApply != null) {
            restoreTo = canvas.save();
        }
        if (offsetForScroll) {
            canvas.translate(mLeft - sx, mTop - sy);
        } else {
            if (!drawingWithRenderNode) {
                canvas.translate(mLeft, mTop);
            }
            if (scalingRequired) {
                if (drawingWithRenderNode) {
                    // TODO: Might not need this if we put everything inside the DL
                    restoreTo = canvas.save();
                }
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }
...省略很多行

可以看到,draw方法会去调用computeScroll()方法,在view中computeScroll其实是一个空方法,
如下所示:

    /**
     * Called by a parent to request that a child update its values for mScrollX
     * and mScrollY if necessary. This will typically be done if the child is
     * animating a scroll using a {@link android.widget.Scroller Scroller}
     * object.
     */
    public void computeScroll() {
    }

很明显,需要我们自定义view的时候,去重写,去实现。上面我们的示例代码,已经实现了该方法,所以才能实现滑动。
下面通过图的方式,来让更好的理解滑动过程

startScroll->invalidate()->draw()->computeScroll()->scrollTo()->
postInvalidate()->draw()->computeScroll()...不断重复,一直完成滑动过程。

最后,看下computeScrollOffset()方法,在computeScroll()方法里面调用的,如下所示:

//先判断mScroller滚动是否完成
if (mScroller.computeScrollOffset()) 

同样,我们看下computeScrollOffset()的源码:

 /**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    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;
    }

代码不是很多,比较容易理解,我们一起梳理下。
在startScroll()方法中把当前的动画毫秒值赋值给了mStartTime,在computeScrollOffset()方法中再一次执行AnimationUtils.currentAnimationTimeMillis()来获取动画毫秒,然后减去mStartTime就是进行了多少时间,然后在跟mDuration进去判断,如果动画进行时间小于我们设置的滚动持续时间mDuration,进去switch的SCROLL_MODE,然后根据Interpolator来计算出在该时间段里面移动的距离,赋值给mCurrX, mCurrY, 所以该方法的作用是,计算在0到mDuration时间段内滚动的偏移量,并且判断滚动是否结束,true代表还没结束,false则表示滚动介绍了。

最后调用scrollTo,则执行了滑动事件。

//这里调用View的scrollTo()完成实际的滚动
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());

好了,整个scroller的源码到这里基本结束了,下一篇讲介绍,更加深入的使用。如果有不清楚的,或者有不对的地方,欢迎留言一起探讨。

Android Scroller源码解析

标签:

原文地址:http://blog.csdn.net/dfskhgalshgkajghljgh/article/details/51993794

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!