从源码角度深入探寻Scroller的奥秘

前言

给未使用过scroller的人说的话
Scroller是一个跟滑动有关的类(大家都这么说(大家:我不承认!?)),很多滑动的操作可以借助Scroller来完成,而且很多有滑动效果的框架啊什么的,都是借助他来完成的,所以如果你们掌握了Scroller类,也能制作那些有意思的滑动效果。

给使用过Scroller的人说的话
你们可能会觉得Scroller有啥讲的,不就是调用startScroll方法,然后重写computeScroll方法不就完了吗,有啥好讲的。对于你们的疑问,我就提一个问题,你们用过Scroller的fling方法吗,啊?用过啊?呵呵,这样啊?;什么!没用过?!那就好办了。

概念

给未使用过scroller的人说的话
关于Scroller这个类,单单只看名字,我们可以觉得这个类隐隐约约跟滚动有点关系,没错,我们经常使用这个类实现一些视图滚动的行为。像是大名鼎鼎的ViewPager,内部也用到了Scroller。所以可见Scroller这个类有多厉害。

给使用过Scroller的人说的话
虽说我们经常使用Scroller实现一些滑动效果,但是老实说,经过我对Scroller的研究,这个类确实跟滑动没有丝毫关系,如果要我定义这个类,这个类应该算是一个由算法合集的工具类,你们其实也知道,在使用Scroller的时候,确实也不能直接那Scroller来做滑动效果,而是利用他计算出来的数据进行单方面的滑动。

用法

在说用法之前,我要大概说明一下,这篇博客的结构,主要是先讲用法,然后我会在源码的基础上探索整个Scroller,如果你的目的只是为了知道要如何使用Scroller,那么你看这个章节就够了。好了,我开始了。

scrollTo和scrollBy

讲用法之前,我们要了解一个知识点,scrollTo(int x, int y)scrollBy(int x, int y)方法,首先要告诉大家的是,这两个方法都可以让View滚动起来。我们来快速的举个例子。

public class MyView extends View {

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
		// 给该view设置点击事件,每点击一次,都会执行一次scrollTo方法
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                v.scrollTo(20, 20);
            }
        });

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 将这个view的大小固定为 500x500
        setMeasuredDimension(500, 500);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 将该view的背景设置为红色
        canvas.drawColor(Color.RED);

        // 在view中间绘制一个半径为50的圆,默认paint,所以这个圆是个黑色的
        canvas.drawCircle(250, 250, 50, new Paint());

    }

}

执行效果:

大家可以看到,我在第一次点击这个view的时候,中间的小黑点确实动了一下,但是之后的点击就没用了,大家也能看到我努力尝试了许久后,最终只能无奈的按下结束录屏= =

紧接着,我们将这个view里面的点击事件里面的scrollTo,改成scrollBy,也就是这样:

setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                v.scrollBy(20, 20);
            }
        });

好的,接下来我们再试试:

这次就不一样了,我们每点击一次,这个小球都会移动一次,要不我们先来总结下?

当我们使用多次scrollTo的时候,小球只会平移一次,使用多次scrollBy的时候,小球会平移多次
(某读者:滚蛋!这算哪门子总结!)

好了好了,我们先思考下,为什么我们使用的scrollTo(20, 20),明明用的正数,小球为什么在向左上角移动,算了,先不多想,记住这个特点就行了。不过我们可以这样记忆。大家都知道手机的坐标系是在手机屏幕的左上角为原点吧,像这样:

所以我们暂且将scrollTo(20, 20)理解为,将手机屏幕上面的原点设为(20, 20),然后既然(20,20)已经是原点了,不如吧这个点放到手机的左上角。那有人可能会提出第二个问题,这个红色的框框为什么没有移动,大家细想,我们使用scrollTo(20, 20)只移动了这个黑色小球,是不是意味着scrollTo只能移动当前view的内容,而不能移动view本身。所以如果我们要移动这个view本身,就应该让这个view的父容器来使用scrollTo方法。

我们还是来看看scrollTo(int x, int y)scrollBy(int x, int y)方法,根据上面的运行结果,我们可以这样思考,scrollTo像是拥有记忆功能,他能记住自己已经移动了(20,20),但是scrollBy就像是一个没有记忆功能的方法,他不知道他曾经移动过,每次调用的时候都会移动一遍。

就像这样:

我们:给我移动(20,20)
scrollTo:移动完了
scrollBy:移动完了
我们:给我移动(20,20)
scrollTo:我已经移动到这里来了
scrollBy:移动完了
我们:给我移动(20,20)
scrollTo:劳资不移动,我特么已经移动到这里来了
scrollBy:移动完了
。。。

所以其实我们可以这样认为,scrollTo永远记得他最开始的位置跟坐标系的位置关系,而scrollBy只记得他现在跟坐标系的位置关系。
假设,我们调用scrollTo(10, 0),再调用scrollTo(20,0),那么内容最终会停留在(20,0)
如果我们调用scrollBy(10, 0),再调用scrollBy(20,0),那么内容最终就会停留在(30,0)

吹逼结束,我们还是来正经看看源码,看源码之前,我先提供一个知识点,view这个类有两个变量:

protected int mScrollX;
protected int mScrollY;

这两个变量记录了这个view移动了多少位置,也就是我们最终移动了多少位置。

好了,知识提供结束,我们来看看scrollBy的源码:

public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

没想到吧,scrollBy还是在scrollTo的基础上进行的操作,只是加了mScrollX和mScrollY,也就是在原来已经移动过的基础上再次进行移动。
所以我们来看看scrollTo的源码:

    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

为了降低脑细胞死亡量,再简化一下看看:

    public void scrollTo(int x, int y) {
        	...
            mScrollX = x;
            mScrollY = y;
            // 滚动到mScrollX和mScrollY的位置
            ...
    }

哈!是不是简单多了,上面不是说了吗,mScrollX和mScrollY记录了最终移动的位置,来看看这里,再结合这个scrollTo,是不是有种scrollTo方法的目的,就是滚动到最终位置。我们把scrollBy的源码scrollTo的源码结合一下:

    public void scrollBy(int x, int y) {
        	...
            mScrollX = mScrollX + x;
            mScrollY = mScrollY + y;
            // 滚动到mScrollX和mScrollY的位置
            ...
    }

scrollBy就是在已经滚动了的基础上再滚动一次。

给大家十秒钟体会一下这两个方法的区别。

好了,我们继续讲scrollToscrollBy,我们来利用他们实现个小功能,让我们自定义的view随着我们的手指运动,既然是随着我们的手指进行运动,那么就不得不重写onTouchEvent方法了,所以最后我们得到以下代码:

public class MyView extends View {

    private Bitmap bitmap;

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.shader1);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int y = (int) event.getY();
                scrollTo(0 - x, 0 - y);
                break;
        }

        return true;
    }

}

运行看看:

也许我们希望按住图片中间任意一点拖动,于是有了以下代码:

public class MyView extends View {

    private Bitmap bitmap;

    // 记录手指刚按下时的坐标
    private float firstX;
    private float firstY;
    
    // 记录总偏移量
    private int sumX;
    private int sumY;

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.shader1);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }
    

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                firstX = event.getX();
                firstY = event.getY();
                break;

            case MotionEvent.ACTION_UP:
                // 记录总偏移量
                sumX = getScrollX();
                sumY = getScrollY();
                break;

            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int y = (int) event.getY();
                // 在上次移动的基础上再次移动
                scrollTo(sumX + (int) firstX - x, sumY + (int) firstY - y);

                break;

        }

        return true;
    }

}

效果:

可能这是你们比较喜欢的效果。

关于scrollerTo和scrollBy大概就讲到这里,我们还是来看看Scroller。

Scroller用法

关于scroller的日常用法,主要有以下三个步骤:

  1. 声明scroller对象
  2. 设置scroller的滚动距离
  3. 重写computeScroll方法

第一点:我想大家都明白,那么其他两点具体怎么做呢,我具体先描述以下,再结合代码进行使用。
第二点:设置scroller的滚动距离的意思是什么意思,其实就是设置我们要把view从哪个位置,滚动到哪个位置,调用scroller的startScroll方法。

startScroll(int startX, int startY, int dx, int dy)

解释下参数:
startX和startY:代表我们选中的参考点的位置,
dx和dy:代表相对于参考点要移动的位置

这样说不知道你们懵不懵,用实际的点举例子好了,假设我们选中(10,10)作为参考点A,现在A点的坐标就是(startX,startY) = (10,10),现在我们相对于A点的偏移(dx,dy) = (20,20),那么实际上我们偏移了多少,如果是以A点为参考系,我们就只偏移了(20,20),但是A点相对于原点已经偏移了(10,10),所以我们实际上相对于原点(手机右上角)偏移了(30,30)。

举个通俗易懂的例子就是,假设有10个椅子排成一排,分别用1到10号表示,让你坐第2个椅子,你肯定就坐2号椅子了,因为你默认1号椅子是第一个,如果我这样说,你坐从2号椅子开始数的第2个椅子,你会坐哪个椅子上,那么我们就会从第二个椅子开始数,答案自然是3号椅子,其实就是这个道理,只是参考物不一样而已。

不过为了让我们的参考系不要那么多变化,我们还是将startX和startY设置为0吧,相对于(0,0)偏移就好了= =

第三点:看到第二点你可能会有疑问,startScroll不是都开始滚动了吗,为啥还要重写computeScroll,话说这个方法是干啥的啊。
的确,startScroll直接翻译过来就是开始滚动的意思,但是这个方法完全没有滚动的功能,不信你看源码(哎呀,看看源码怎么了嘛):


    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;
    }

看到了吧?就是赋值,其他啥都没有了。
既然使用startScroll并没有什么卵用,所以我们要重写viewcomputeScroll方法,所以这个方法到底是干啥用到,我没打算带大家走一遍view的源码,这样就太长了,所以还是直接告诉你们答案吧,当view发生滚动的时候,会调用一次这个方法,所以连续滚动的时候就会连续调用这个方法,还记得滚动会发生啥事吧,如果你还记得上面我提到的:

protected int mScrollX;
protected int mScrollY;

这两个变量,你现在就可以这样理解,view进行滚动,也就是mScrollXmScrollY发生变化的时候,就会调用computeScroll方法。
那么我们怎么重写这个方法呢,老实说,还有最后一步就能使用scroller滚动了,好开森,你特么倒是快点讲啊,磨磨唧唧的。

讲这个方法之前,我要告诉大家一个噩耗。唉~,Scroller不能实现滑动功能!

WTF?!不能实现滑动还搞这么多幺蛾子?

老实说,即便使用了Scroller,但若要view滑动,还得靠scrollByscrollTo,不然你以为我为什么要花大量篇幅来讲解这两个方法?

在讲解怎么重写computeScroll方法之前,我先讲Scroller的一个很重要的方法,computeScrollOffset,这个方法有一个boolean返回值,表示滚动行为是否结束,并且每调用一次,我们都可以再通过scroller得到一个位置信息,这个位置我称为:当前view应该滚动到的位置。

所以当你调用了Scroller的computeScrollOffset方法后,你就能够得到当前view应该滚动到什么位置,然后调用scrollByscrollTo进行实际的滚动了,所以到最后还是得靠scrollByscrollTo

我们来看看具体怎么重写,直接上完整代码,我相信你们看码的压力应该也不会那么大了:

public class MyView extends View {

    private Bitmap bitmap;
    private Scroller scroller;

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.shader1);
        scroller = new Scroller(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                scroller.startScroll(100, 100, 300, 300);
                invalidate();
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
        }

    }
}

再讲一下这个computeScroll方法,已经说了,当view有滚动行为的时候,才会调用这个方法,这里面的流程大概是这样:

if判断:滚动行为未完成,进入if内部
得到应该滚动到哪个位置
开始滚动(出现滚动!调用computeScroll
if判断:滚动行为未完成,进入if内部
得到应该滚动到哪个位置
开始滚动(出现滚动!调用computeScroll

if判断:滚动行为已完成,结束!

就不画流程图了,万一我画的流程图,你们看不懂还要思索一会儿才能看懂= =

实践:实现一个劣质的ViewPager

什么叫实现一个劣质的viewpager,就是只实现viewpager的滑动效果,也没有setAdapter之类的方法,来看看效果

虽然名义上说的是一个劣质的viewpager,不过效果看着还不错,不是吗。
本来打算贴上所有源码就跑路,但是觉得这样不负责任,所以我们还是来细细的说一下实现思路。

首先这肯定是一个viewgroup,所以我们要自定义一个ViewGroup咯!

看看在哪里会用到Scroller,首先我们在这个自定义的ViewGroup中添加了3个子view,手指在屏幕上滑动的时候,我们肯定用的是scrollTo或者scrollBy,具体使用哪个方法就要看个人喜好了。

当我们手指离开屏幕的时候,要作判断,最终需要定位到哪个item,然后自动滑到合适的item,这里的自动滑动,自然就要派scroller登场了。

首先我们按照正常的自定义一个viewgroup的流程开始,重写onMeasure方法计算每个view的大小,再重写onLayout方法,规定每个view在这个viewgroup的位置,所以顺其自然的,出现了以下代码。

public class BadViewPager extends ViewGroup {

    public BadViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            // 为每个子view测量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if (changed) {

            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                // 让每个子控件都是屏幕宽度,并且水平布局
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
        }

    }

}

现在我们来使用一下这个容器:

    <com.example.BadViewPager
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#f0f" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ff0" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#0ff" />

    </com.example.BadViewPager>

目前的效果是这样的:

但是现在还不能滑动,我们根据上面讲的内容,做一下手指触摸滑动的处理,当然,只需要水平滑动就可以了,所以在使用scrollTo或者scrollBy的时候,就不需要传递在Y轴上的变化了,所以就变成了这个样子:

public class BadViewPager extends ViewGroup {

    private float firstX;
    private int sumX = 0;

    public BadViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            // 为每一个子控件测量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if (changed) {

            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                // 让每个子控件都是屏幕宽度,并且水平布局
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
        }

    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                firstX = event.getX();
                break;

            case MotionEvent.ACTION_UP:
                sumX = getScrollX();
                invalidate();
                break;

            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int scrollX = sumX + (int) firstX - x;
                scrollTo(scrollX, 0);
                break;
        }

        return true;
    }

}

效果图:

嗯,还算有模有样,接着我们加上scroller,并且当手指离开屏幕的时候,判断一下应该滚动到哪个item,然后借助scroller自动滚到对应的位置,所以最终的代码就是这样的:

public class BadViewPager extends ViewGroup {

    private float firstX;

    private Scroller scroller;
    
	// 移动多少,就认为是发生了滑动行为
    private int slop;

    int sumX = 0;

    // 当前item
    float currItem = 0;

    public BadViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        scroller = new Scroller(context);
        ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
        slop = viewConfiguration.getScaledPagingTouchSlop();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            // 为每一个子控件测量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if (changed) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                // 让每个子控件都是屏幕宽度,并且水平布局
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                firstX = event.getX();
                break;

            case MotionEvent.ACTION_UP:

                currItem = Math.round((float) getScrollX() / getWidth());
                int dx = (int) (currItem * getWidth() - getScrollX());
                scroller.startScroll(getScrollX(), 0, dx, 0);

                // 记录总偏移量
                sumX = (int) (currItem * getWidth());

                invalidate();

                break;

            case MotionEvent.ACTION_MOVE:

                int x = (int) event.getX();
                int scrollX = sumX + (int) firstX - x;
                scrollTo(scrollX, 0);

                // 限制每个页面的边界
                if (scrollX < 0) {
                    scrollTo(0, 0);
                } else if (scrollX > (getChildCount() - 1) * getWidth()) {
                    scrollTo((getChildCount() - 1) * getWidth(), 0);
                } else {
                    scrollTo(scrollX, 0);
                }

                break;
        }

        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            int currX = scroller.getCurrX();
            scrollTo(currX, 0);
            invalidate();
        }

    }
}

效果图跟最开始给大家展示的效果一样,就不再给大家展示一遍了。

如果你的目的只是为了知道scroller的用法,我相信你看了以上的篇章,应该就知道了,也许部分东西还不是很清楚,那么我建议你再阅读一遍用法篇,在阅读的过程中同时进行手动操作。因为在我的思想中,要学会一个知识,光看一遍知识讲解,是无法掌握这个知识的,需要你手动操作,在手动操作的时候,你就会发现哪些东西还不够清楚,然后针对不清楚的地方,再反复钻研,相信你很快就能掌握这个知识点。

探索scroller原理

在我研究scroller的时候,发现scroller是一个很特别的类,它不依赖其他类,它就像是一个独立存在的类。你可以做这样一个操作:新建一个类,然后将scroller的源码全部复制进来,你会发现这个类都不会报错。以这种形式存在的类,在Android源码里确实算很少见的了,所以它极大的增加了我研究它的兴趣,再加上scroller的源码不算太多,所以我们来研究它也不会显得压力太大。

不过本篇幅讲的过于细节(我可能会一行一行的讲解源码),会很长,如果你决定看了,希望你还是静下心来细细研读,这里的研读,是希望在看我的文章的同时,也要仔细看我提供的scroller源码,我会尽量让你在一个舒适的环境阅读源码。

我这里的建议就是,希望你看完本篇章之后,你也去看看scroller的全部源码,没错,就是全部,如果遇到难以理解的,再回来看看这篇博客,希望能够对你有所帮助。

那我们就开始吧!

构造方法

既然是研究这个类,那么我们就从这个类的构造方法开始研究,进入源码,我们会发现这个类有3个构造方法,作为刚开始研究,自然从参数最少的构造方法开始看,这样我们的压力不会太大:

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

我们发现最简单的构造方法调用了另一个构造方法,我们进去:

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

Context参数我们就不讲了,直接来看看第二个参数Interpolator,这是一个差值器,一般差值器都是用来控制数据变化的趋势的,比如我们可以让数据匀速变化,也可以让数据开始慢,后来快,这些都需要使用到差值器来实现,既然scroller里面用到了这个东西,我们可以认为scroller在做滑动行为的时候,我们可以通过这个Interpolator来决定滑动的趋势,先快后慢,或者匀速运动之类的。

这个构造方法里面又调用了一个构造方法,所以我们来看看第三个构造方法:

    // 差值器
    private final Interpolator mInterpolator;
    
    // 结束
    private boolean mFinished;
    // 飞轮?
    private boolean mFlywheel;

    // 减速
    private float mDeceleration;
    // PPI是Pixels Per Inch缩写,pixels per inch所表示的是每英寸所拥有的像素(pixel)数目。(参考百科)
    private final float mPpi;
    
    // A context-specific coefficient adjusted to physical values.
    // 根据物理值调整的特定于上下文的系数。
    private float mPhysicalCoeff;

    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
    }

看到这么多源码,是不是有点想打退堂鼓了?没关系,慢慢来嘛,我故意将这个构造方法里面的变量也原封不动的复制了过来,就是为了让你能在一个良好的环境下阅读,里面的变量我做了翻译,但是这种翻译并不是一定就是正确的翻译,只是站在一个刚刚看源码的人的角度做的翻译,所以即便有了翻译,这可能也不是这个变量本身的意义。

跟我一起来一行一行的阅读这些源码,我们发现这些源码都是给变量赋值,

 mFinished = true;

默认为true,知道就行了,继续:

       if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }

差值器赋值,如果我们没有给scroller传递一个差值器,那么scroller就会自己使用一个默认的差值器,其中这个默认的差值器ViscousFluidInterpolator其实是scroller的一个内部类,是scroller内部实现的一个差值器,我就不复制代码了,大家知道scroller内部有一个ViscousFluidInterpolator类就行了。

mPpi = context.getResources().getDisplayMetrics().density * 160.0f;

屏幕PPI,关于dp的官方叙述为当屏幕每英寸有160个像素时(也就是160dpi),dp与px等价的。所以这样是通过屏幕密度转化成了以像素为单位的长度,也就是px。

mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());

这里出现了变化,调用了一个方法才计算出这个值,我们先来看看ViewConfiguration.getScrollFriction()这个能得到什么东西,这个方法得到的是滚动摩擦系数,也就是摩擦系数,不知道你们是否还记得高中学的物理力学,里面有这么一个东西。

一个物体在粗糙的表面上受一个拉力F,其中这个表面的摩擦系数为u,这时,这个物体会在这个表面进行匀加速直线运动,根据牛顿第二定律,这个物体的合力就是F拉 - f摩 = ma,然后这里面的f摩 = mgu
如果这个物理的拉力突然消失,但是物体已经拥有一个速度了,不会突然停止。因为这个物体拥有动能,将动能转化为内能后,这个物体才会停止运动,假设拉力消失后,物体的速度为v,此时根据动能公式,该物体目前的动能为E动 = 1/2mv^2,物体要运行多久才会停止呢,由于当前已经没有拉力存在了,所以目前摩擦力做功,所以1/2mv^2 = mguL,这个L就是物体失去拉力运动后的距离。

以上物理知识只是我们看到ViewConfiguration.getScrollFriction()莫名其妙想到的,跟源码无关啊,我们继续来看源码:

mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());

还是这行源码,里面有个computeDeceleration方法,翻译过来计算减速,然后计算减速的时候需要传递一个摩擦因数,我们进这个方法一探究竟:

    private float computeDeceleration(float friction) {
        return SensorManager.GRAVITY_EARTH   // g (m/s^2)
                      * 39.37f               // inch/meter
                      * mPpi                 // pixels per inch
                      * friction;
    }

老实说,我对这个方法百思不得其解,查了很多资料不知道这个公式怎么来的,不过最后我还是有一个想法,首先我看到了39.37f这个常量,我发现一米就不偏不倚的刚刚好等于39.37英寸,39.37英寸*ppi,也就是一米有多少个像素。这个方法还用到了重力加速度g,到底要怎么理解呢,我强行理解成了如下:质量为1的物体,移动1米摩擦力做的功。然后套上这个刚刚好,E摩擦力 = mguL,其中m = 1,g = 9.8,u = ViewConfiguration.getScrollFriction(),L = 1米。刚刚好!所以mDeceleration就是质量为1的物体移动1米摩擦力做的功。
接着看源码:

 mFlywheel = flywheel;

这个赋值就不说了,继续:

mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning

这里也调用了computeDeceleration方法,不过后面居然有个注释,翻译过来好像是google的工程师经过测试发现摩擦系数为0.84的时候,给人的感觉是最佳的。

现在总算是把构造方法给看完了,那么我们接下来应该看什么呢。
既然不知道应该看什么方法,那么我们就来看startScroll方法吧。

startScroll

先看看源码:

    private static final int DEFAULT_DURATION = 250;
    
    public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    }

内部又调用了startScroll方法,我们进这个方法看看:

    // 模式
    private int mMode;
    // 结束
    private boolean mFinished;
    // 时间
    private int mDuration;
    // 开始时间
    private long mStartTime;
    // 开始X位置
    private int mStartX;
    // 开始Y位置
    private int mStartY;
    // 结束X位置
    private int mFinalX;
    // 结束Y位置
    private int mFinalY;
    
    private float mDeltaX;
    private float mDeltaY;
    
    private float mDurationReciprocal;

    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:代表我们选中的参考点的位置,
dx和dy:代表相对于参考点要移动的位置
这样说不知道你们懵不懵,用实际的点举例子好了,假设我们选中(10,10)作为参考点A,现在A点的坐标就是(startX,startY) = (10,10),现在我们相对于A点的偏移(dx,dy) =
(20,20),那么实际上我们偏移了多少,如果是以A点为参考系,我们就只偏移了(20,20),但是A点相对于原点已经偏移了(10,10),所以我们实际上相对于手机偏移了(30,30)。

举个通俗易懂的例子就是,假设有10个椅子排成一排,分别用1到10号表示,让你坐第2个椅子,你肯定就坐2号椅子了,因为你默认1号椅子是第一个,如果我这样说,你坐从2号椅子开始数的第2个椅子,你会坐哪个椅子上,那么我们就会从第二个椅子开始数,答案自然是3号椅子,其实就是这个道理,只是参考物不一样而已。

这里多了一个参数duration,代表从A点移动到B点所消耗的时间。

所以你告诉了scroller要移动的点的位置,并且也告诉了scroller要移动多少距离,在个方法里面,scroller已经定好了移动需要花费的所有时间,并且这个方法里面,scroller已经计算好了移动的最终位置finalX和finalY。

这个方法我们就不看了,不过要注意的是,当前的滚动模式mModeSCROLL_MODE

接下来我们来看看相对比较复杂的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;
                    // 100个里面应该选择哪一个
                    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;
    }

注意这里面有一个switch分支,刚刚在看startScroll方法的时候,我已经强调startScroll用的是SCROLL_MODE,所以我们简化一下上面的代码:

    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;
            }
        } else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

一点一点的来看,第一个if

        if (mFinished) {
            return false;
        }

看来mFinished这个变量是作为一个标识存在的,如果finished了,这个方法也就没有进去的必要了。

 int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);

这个变量,用当前时间减去开始时间,也就得到了已经过去了多少时间,也就是调用startScroll之后已经过去了多少时间了。

        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;
            }
        } else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }

if判断,是否到了规定时间。

进入SCROLL_MODE分支,我们发现,在这里,mCurrXmCurrY被赋值了。

注意!当调用了computeScrollOffset方法后,mCurrXmCurrY才正式被赋值。

然后我们就可以使用scrollergetCurrXgetCurrY方法,得到当前应该被移动到的位置。

最后我们使用viewscrollToscrollBy移动。

所以scroller给我们一种这样的感觉,我们调用startScroll指定了开始位置和结束位置,并规定了从开始位置到结束位置需要花费多少时间,当我们在这段时间使用computeScrollOffset后,computeScrollOffset内部才根据已经过去了的时间计算应该走到哪个位置,然后我们通过getCurrXgetCurrY方法得到这个位置。

所以scroller也不过如此是吗,这样的话,我用ValueAnimator也能实现相同的功能,而且至少ValueAnimator是在时时的变化。你这样说也没错,确实只用ValueAnimator也能实现相同的效果,不过scroller使用起来更简单不是吗。何况scroller还有复杂的fling滚动,并且我们可以说scroller是为滚动而生的一个类,google的工程师考虑到了很多物理上的因素,让我们在滚动的时候,看着很舒服很自然。

接下来我们要说说scrollerfling了,在说这个之前,我们需要先看一个东西,scroller类里面有一个静态代码块。

静态代码块

scroller的静态代码块,这意味着什么,在还没有使用到startScroll的时候,scroller内部已经有代码开始运作了,我们来看看这段代码长什么样子:


    private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
    private static final float START_TENSION = 0.5f;
    private static final float END_TENSION = 1.0f;
    private static final float P1 = START_TENSION * INFLEXION;
    private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);

    private static final int NB_SAMPLES = 100;
    private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
    private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];

    static {
        float x_min = 0.0f;
        float y_min = 0.0f;
        for (int i = 0; i < NB_SAMPLES; i++) { // NB_SAMPLES = 100
            // 百分之几
            final float alpha = (float) i / NB_SAMPLES; // 0~1

            float x_max = 1.0f;

            float x, tx, coef;

            while (true) {
                x = x_min + (x_max - x_min) / 2.0f;
                coef = 3.0f * x * (1.0f - x);
                tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
                if (Math.abs(tx - alpha) < 1E-5) break;
                if (tx > alpha) x_max = x;
                else x_min = x;
            }
            // SPLINE_POSITION = 100 + 1
            SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;

            float y_max = 1.0f;
            float y, dy;
            while (true) {
                y = y_min + (y_max - y_min) / 2.0f;
                coef = 3.0f * y * (1.0f - y);
                dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
                if (Math.abs(dy - alpha) < 1E-5) break;
                if (dy > alpha) y_max = y;
                else y_min = y;
            }
            SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
        }
        SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
    }

这段代码看着恐惧吗。
老实说这段代码将我折磨的死去活来的,因为很难理解里面的式子,我最开始尝试在表面张力的角度去思考这段代码,因为TENSION有张力的意思,然后我发现这条路走不通,后来我觉得TENSION可能是拉力的意思,然后我重温了高中力学,感觉还稍微靠了点边,不过还是没有完全吃透这堆代码。

本来打算这这篇文章中写下我思考和探索这堆静态代码块的过程以及进度,但是由于我自己也不能确定是否正确,所以不敢误人子弟。

也就不带大家细细品读这堆代码,我就直接说结果好了。

这个代码块的主要目的是为了给SPLINE_POSITIONSPLINE_TIME这两个数组赋值,一个位置一个时间,将时间和位置分成精度为0.00001的样子,然后以百分比的形式投放到数组中。

我这样说,你可能也不一定能够特别明白,所以我做了一件这样到事情,将这两个数组都打印了出来,我就得到了两列数据,然后我将这两列数据利用Excel做成了折线图:

这里的时间是以百分比的形式出现的,这里的位置是以小数的形式出现的,其实都差不多。
我们通过这张图可以清楚的知道时间与位置的关系。
当时间走了百分之多少了,我们可以拿到对应的位置走了百分之多少。

这个静态代码块我们就算过去了,接下来我们来看看fling方法

fling

源码:

    public void fling(int startX, int startY, int velocityX, int velocityY,
                      int minX, int maxX, int minY, int maxY) {
        // Continue a scroll or fling in progress
        if (mFlywheel && !mFinished) {
            float oldVel = getCurrVelocity();

            float dx = (float) (mFinalX - mStartX);
            float dy = (float) (mFinalY - mStartY);
            float hyp = (float) Math.hypot(dx, dy); // 假设dx和dy为直角三角形的两条直角边,hyp则为斜边长

            float ndx = dx / hyp;
            float ndy = dy / hyp;

            float oldVelocityX = ndx * oldVel;// 计算X边的速度
            float oldVelocityY = ndy * oldVel;// 计算Y边的速度

            // Math.signum 判断正负数,参数为正,返回1.0,参数为负返回-1.0,参数为0返回0
            if (Math.signum(velocityX) == Math.signum(oldVelocityX)
                    && Math.signum(velocityY) == Math.signum(oldVelocityY)) {
                velocityX += oldVelocityX;
                velocityY += oldVelocityY;
            }
        }

        mMode = FLING_MODE;
        mFinished = false;

        float velocity = (float) Math.hypot(velocityX, velocityY);

        mVelocity = velocity;
        mDuration = getSplineFlingDuration(velocity);
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;

        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;

        double totalDistance = getSplineFlingDistance(velocity);
        mDistance = (int) (totalDistance * Math.signum(velocity));

        mMinX = minX;
        mMaxX = maxX;
        mMinY = minY;
        mMaxY = maxY;

        mFinalX = startX + (int) Math.round(totalDistance * coeffX);
        // Pin to mMinX <= mFinalX <= mMaxX
        mFinalX = Math.min(mFinalX, mMaxX);
        mFinalX = Math.max(mFinalX, mMinX);

        mFinalY = startY + (int) Math.round(totalDistance * coeffY);
        // Pin to mMinY <= mFinalY <= mMaxY
        mFinalY = Math.min(mFinalY, mMaxY);
        mFinalY = Math.max(mFinalY, mMinY);
    }

看源码之前,我先说说这个方法的各个参数是啥:

int startX:起始位置X
int startY:起始位置Y
int velocityX:X轴上的速度
int velocityY:Y轴上的速度
int minX:X轴上最小移动的距离
int maxX:X轴上最大移动的距离
int minY:Y轴上最小移动的距离
int maxY:Y轴上最大移动的距离

虽然我已经描述了每个参数是干啥的,但是你们也不一定知道是怎么用的。

所以我还是讲一下这里面的参数是干啥用的,讲解之前,我们要知道fling是一个怎样的操作,可能有很多人已经知道了,不过我还是说一下,fling操作其实就是,比如这里有一个列表,我们手指触摸在屏幕上滑动,然后手指松开,这个列表依然在继续滑动,仿佛列表有惯性似的。就像我们小时候玩纸飞机,飞机还在我们手上的时候,我们手怎么动,飞机就怎么动,当我们放开手,飞机不会马上停止,而会在手放开的那个方向上继续飞行,就是有惯性的意思。这种放开手物体还在运动的行为,我们称为fling。

好了,接下来我们来讲解下fling的这几个参数,前4个参数应该不用讲了吧,不过还是讲一下好了。
startX和startY:代表飞机离开手指时候的位置
velocityX和velocityY:飞机离开手指的时候,飞机在X轴和Y轴上的速度,速度带有方向,可能是正的可能是负的,这个别忘了。
minX和maxX:飞机在X轴上运行的范围
minY和maxY:飞机在Y轴上运行的范围

这样解释是不是要好理解一些。

说到fling方法,好像我上面没有说fling应该怎么用,就在这里补充一下吧。

fling用法

其实fling和startScroll用法一样,只是参数不一样。
还记得之前讲的startScroll怎么用吗,三步:

  1. 声明Scroller
  2. 调用startScroll
  3. 重写view的computeScroll方法

这里fling也是三步:

  1. 声明Scroller
  2. 调用fling
  3. 重写view的computeScroll方法

不过fling方法要传递两个速度值,这速度值应该怎么计算呢,google提供了一个VelocityTracker类,专门用来计算速度,这个类怎么用呢,就不让你们还专门去查一下了,我这里快速说一下。
首先声明一下这个类

VelocityTracker velocityTracker;

然后在构造方法里实例化这个变量(也不一定非要在构造方法里面实例)

velocityTracker = VelocityTracker.obtain();

然后在onTouchEvent里面添加相应事件

velocityTracker.addMovement(event);

手指抬起的时候调用这个方法:

velocityTracker.computeCurrentVelocity(500, Float.MAX_VALUE);

里面的参数我说明一下,第一个参数表示获取多少毫秒内的速度,第二个参数表示允许的最大速度,
我这里设置的是500毫秒,最大速度我设置的是Float的最大值,你速度想多快就多快。

所以整体大概长这样。

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        velocityTracker.addMovement(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:

                velocityTracker.computeCurrentVelocity(500, Float.MAX_VALUE);
                int xVelocity = (int) velocityTracker.getXVelocity();
                int yVelocity = (int) velocityTracker.getYVelocity();

                break;

        }

        return true;
    }

然后我们就可以得到X和Y轴上的速度了。

如果要知道VelocityTracker的详细使用方法,还是还是去查一下相关资料吧,我这里重点不是讲这个,就粗略的说一下好了。

好了,现在我们知道怎么得到速度了,现在我们就开始使用fling方法吧。
我是这样使用的:

scroller.fling(sumX, sumY,  -xVelocity, -yVelocity, 
	-Integer.MAX_VALUE, Integer.MAX_VALUE, -Integer.MAX_VALUE, Integer.MAX_VALUE);

不限制他的范围,直接给最大范围。
整个自定义view的代码就长这样:

public class ScrollerView extends View {

    private final Scroller scroller;
    private float firstX;
    private float firstY;
    private Bitmap bitmap;

    private VelocityTracker velocityTracker;

    private boolean isfling = false;

    private int sumX = 0;
    private int sumY = 0;

    public ScrollerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.shader1);
        scroller = new Scroller(context);
        velocityTracker = VelocityTracker.obtain();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        velocityTracker.addMovement(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:

                velocityTracker.computeCurrentVelocity(500, Float.MAX_VALUE);
                int xVelocity = (int) velocityTracker.getXVelocity();
                int yVelocity = (int) velocityTracker.getYVelocity();

                sumX += firstX - event.getX();
                sumY += firstY - event.getY();

                scroller.fling(sumX, sumY, -xVelocity, -yVelocity,
                        -Integer.MAX_VALUE, Integer.MAX_VALUE, -Integer.MAX_VALUE, Integer.MAX_VALUE);

                invalidate();

                break;

            case MotionEvent.ACTION_DOWN:
                firstX = event.getX();
                firstY = event.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int y = (int) event.getY();
                scrollTo(sumX + (int) firstX - x, sumY + (int) firstY - y);
                break;
        }

        return true;
    }

    @Override
    protected void onDetachedFromWindow() {
        velocityTracker.recycle();
        super.onDetachedFromWindow();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();

        if (scroller.computeScrollOffset()) {

            isfling = true;

            int currX = scroller.getCurrX();
            int currY = scroller.getCurrY();

            scrollTo(currX, currY);
            invalidate();
            
        } else {

            if (isfling) {
                sumX = getScrollX();
                sumY = getScrollY();
            }
            isfling = false;

        }

    }
}

一行注释都没有,不知道你们看着会不会吃力,不过既然能看到这里来,耐心也是非比寻常,相信你们!
来看看效果图:

大概就是这样,手指离开屏幕后,图片还是会滑动一段距离。用法就讲到这里吧,跟startScroll用法差不多就不赘述了。

fling源码

我们接着再来讲fling的源码。
其实fling方法的源码跟startScroll方法的本质是一样的,都是各种赋值,只不过fling要稍微麻烦一点,并且将模式设置成了

mMode = FLING_MODE;

所以我们继续看computeScrollOffset方法,不过这次就只看FLING_MODE下的方法了

    public boolean computeScrollOffset() {

        int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);

        if (timePassed < mDuration) {
            switch (mMode) {
                case FLING_MODE:
                    // 时间过去了百分之多少
                    final float t = (float) timePassed / mDuration;
                    // 100个里面应该选择哪一个
                    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的思路一样,只有在调用了computeScrollOffset方法,scroller内部才会进行计算,计算应该滚动到哪个位置。不过这里的计算并不是依赖差值器,而是依赖静态代码块中计算出来的SPLINE_POSITIONSPLINE_TIME这两个数组的值,还记得这两个数组中保存这什么东西吗,时间和位置的关系。

也就是这个。

到现在为止,scroller的主要部分的源码就算被我们探索完了。

总结

scroller主要提供了两种方式做滑动,正经的滑动startScroll和有抛掷行为的fling,这两种方法的使用步骤都一样:

  1. 声明Scroller
  2. 调用startScroll或者fling方法
  3. 重写view的computeScroll方法

他们的原理都是一样的,使用startScroll或者fling的时候,记录当前状态,位置速度什么的,当你调用scroller的computeScrollOffset方法之后,scroller内部会根据时间差飞快的计算当前时间点应该移动到哪个位置,然后使用scroller的getCurrX和getCurrY就可以得到这个位置了。

现在听原理倒是挺简单的,但是scroller内部用了很多复杂的算法来计算当前应该移动到哪些位置,相信google的那些工程师在思考这些算法的时候下了不少功夫,不仅要考虑到滑动的物理效果,还要考虑到视觉上的美观,虽然这只是一个单独的类,不过在整个探索的过程中,真是下了不少功夫才摸个七七八八。

那么关于Scroller的探索就到这咯!

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值