打破认知Scroller你真的了解吗

2020-10-15 08:45 Android技术之家

作者:你缺少想象力

链接:

https://blog.csdn.net/IT_XF/article/details/83344780

本文由作者授权发布。


这篇文章非常长...长到了已经超出一篇文章的限制,正因为如此,这篇文章把Scroller探寻的非常透彻,不管你熟不熟悉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(2020);
            }
        });

    }

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

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

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

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

    }

}


执行效果:



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


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


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


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



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


当我们使用多次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就是在已经滚动了的基础上再滚动一次。


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


好了,我们继续讲scrollTo和scrollBy,我们来利用他们实现个小功能,让我们自定义的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, 00null);
    }

    @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, 00null);
    }


    @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的日常用法,主要有以下三个步骤:


  • 声明scroller对象

  • 设置scroller的滚动距离

  • 重写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并没有什么卵用,所以我们要重写view的computeScroll方法,所以这个方法到底是干啥用到,我没打算带大家走一遍view的源码,这样就太长了,所以还是直接告诉你们答案吧,当view发生滚动的时候,会调用一次这个方法,所以连续滚动的时候就会连续调用这个方法,还记得滚动会发生啥事吧,如果你还记得上面我提到的:


protected int mScrollX;
protected int mScrollY;


这两个变量,你现在就可以这样理解,view进行滚动,也就是mScrollX和mScrollY发生变化的时候,就会调用computeScroll方法。


那么我们怎么重写这个方法呢,老实说,还有最后一步就能使用scroller滚动了,好开森,你特么倒是快点讲啊,磨磨唧唧的。


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


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


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


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


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


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


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, 00null);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                scroller.startScroll(100100300300);
                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" />


    </com.example.BadViewPager>


目前的效果是这样的:



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


public class BadViewPager extends ViewGroup {

    private float firstX;
    private int sumX = 0;


    @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 {

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {

            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(00);
                } 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, 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());


还是这行源码,里面有个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。


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


接下来我们来看看相对比较复杂的computeScrollOffset方法。


注意这里面有一个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分支,我们发现,在这里,mCurrX和mCurrY被赋值了。

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

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

最后我们使用view的scrollTo或scrollBy移动。


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


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



篇幅原因,这里省略了一节:静态代码块


接下来我们要说说scroller的fling了。


fling


    public void fling(int startX, int startY, int velocityX, int velocityY,
                      int minX, int maxX, int minY, int maxY)
 


我先说说这个方法的各个参数是啥:


  • 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的这几个参数。


  • 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轴上的速度了。


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


我是这样使用的:


scroller.fling(sumXsumY,  -xVelocity-yVelocity
-Integer.MAX_VALUEInteger.MAX_VALUE-Integer.MAX_VALUEInteger.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, 00null);
    }

    @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源码点击阅读原文吧,为了删篇幅我也是操碎了心,已经到达文章内容极限了....


关注我获取更多知识或者投稿


本文章转载自公众号:jszj2014215

首页 - Android 相关的更多文章: