从源码角度深入探寻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)偏移就好了

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值