学习Scroller类看这篇就够了

老实说这个Scroller类不太好理解,我看了好几遍才明白怎么回事,希望对大家有所帮助。

1.概念

Scroller是一个处理滚动效果的工具类,可能这么说大家太抽象,但是如果我跟你说咱们常用的ViewPager、ListView中都使用了Scroller类,你可能就有形象的概念了。下面看一个demo的效果图,本章对Scroller类做详细的介绍后,咱就做一个这个效果:

在上面的效果图中,当点击上面的button后,就做一次滑动。

2.View类关于滑动的方法介绍

在介绍Scroller之前必须要对View中跟滑动相关的方法进行介绍,因为我们的scroller必须依托与它们才能实现。

大家玩过lol吗,假设现在你想从上路到下路你会如何做呢?

方法1:带传送,刷一下就过去了。
方法2:慢慢走过去。(用来手机上就上平滑的移动过去)

第一种方法在view类中已经给实现好,view中有scrollBy和scrollTo2种方法;而第二种方法就是我们要用scroller实现的功能。

2.1介绍scrollBy()和scrollTo()的区别

scrollBy()方法是让View相对于当前的位置滚动某段距离。
scrollTo()方法则是让View相对于初始的位置滚动某段距离。

这么说可能理解起来有点费劲,我们做个小例子:

我们来看一下activity_main.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.example.guolin.scrollertest.MainActivity">

    <Button  android:id="@+id/scroll_to_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="scrollTo"/>

    <Button  android:id="@+id/scroll_by_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="scrollBy"/>

</LinearLayout>

再来看一下MainActivtiy.class类:

public class MainActivity extends AppCompatActivity {

    private LinearLayout layout;

    private Button scrollToBtn;

    private Button scrollByBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        layout = (LinearLayout) findViewById(R.id.layout);
        scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);
        scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);
        scrollToBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                layout.scrollTo(-60, -100);
            }
        });
        scrollByBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                layout.scrollBy(-60, -100);
            }
        });
    }
}

代码很简单,功能如下:

当点击了scrollTo按钮时,我们调用了LinearLayout的scrollTo()方法,
当点击了scrollBy按钮时,调用了LinearLayout的scrollBy()方法。

运行效果如下:

可能有读者有以下问题:

1.为什么传入的是坐标是负数,反而正着跑
2.为什么不是调用button的scrollTo和ScrollBy而是调用父布局Linearlayout的这二个方法呢?

相信大家都有这个疑问?别急,我们后面再给大家解答。我们先得把scrollTo和ScrollBy的区别说完。

再来看看这二个方法的区别,这次相信大家有点明白了吧!

1.scrollTo()方法则是让View相对于初始的位置滚动某段距离。因为我们传入的一直是相同的x,y值(注意是值而不是点!值表示移动这么长的距离,点表示移动到该点上),所以只在第一次点击的时候变化,往后点击就不会变化了。
2.scrollBy()方法是让View相对于当前的位置滚动某段距离。所以每次点击它都会在当前位置上再移动一段距离。

帮助大家更好的理解,下面我们来看看scrollTo和ScrollBy的源码。


/** 
   * Set the scrolled position of your view. This will cause a call to 
   * {@link #onScrollChanged(int, int, int, int)} and the view will be 
   * invalidated. 
   * @param x the x position to scroll to 
   * @param y the y position to scroll to 
   */  
  public void scrollTo(int x, int y) {  
//看到没?核心在if判断上,如果只有在我们的参数和上一次的值不一样的进候,我们才进行移动。
      if (mScrollX != x || mScrollY != y) {  
          //下面这二句保存上一次的值
          int oldX = mScrollX;  
          int oldY = mScrollY;  
          //下面这二句记录最新的参数值
          mScrollX = x;  
          mScrollY = y;  

          invalidateParentCaches();
          onScrollChanged(mScrollX, mScrollY, oldX, oldY);
          if (!awakenScrollBars()) {
              postInvalidateOnAnimation();
          }
      }  
  }  

从该方法中我们可以看出,先判断传进来的(x, y)值是否和View的mScrollX, mScrollY偏移量相等,如果不相等,就调用onScrollChanged()方法来通知界面发生改变,然后重绘界面,所以这样子就实现了移动效果啦,因为第一次点击的时候mScrollX, mScrollY都是0,所以会移动,从第二次开始就没有效果了。

/** 
    * Move the scrolled position of your view. This will cause a call to 
    * {@link #onScrollChanged(int, int, int, int)} and the view will be 
    * invalidated. 
    * @param x the amount of pixels to scroll by horizontally 
    * @param y the amount of pixels to scroll by vertically 
    */  
   public void scrollBy(int x, int y) {  
       scrollTo(mScrollX + x, mScrollY + y);//在原来的偏移量上加上最新的偏移量,它就会在原来的基础上向前移动。  
   }  

原来scrollBy里面调用了scrollTo()方法!这个大家都明白了吧。

2.2填坑

还记得上面我们没有解决的2个坑吗?

1.为什么传入的是坐标是负数,反而正着跑。
2.为什么不是调用button的scrollTo和ScrollBy而是调用父布局Linearlayout的这二个方法呢?

下面我们一个一个来解决。

填坑1.为什么传入的是坐标是负数,反而正着跑。

在做分析之前我们得了解Android设备屏幕的平面直角坐标系概念。在Android手机中,屏幕的直角坐标系概念简单来说:

屏幕左上角为直角坐标系的原点(0,0)
从原点出发向左为X轴负方向,向右为X轴正方向
从原点出发向上为Y轴负方向,向下为Y轴正方向

请记住以上三条,通过坐标轴能形象的标明,如下图:

我们知道,我们都是调用invalidate()方法促使UI的重新绘制,这里我们直接定位到这个方法中去。

 public void invalidate(int l, int t, int r, int b) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
    }

看这一行代码:
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
根据“负负得正”的道理,“- scrollX”和“- scrollY” ,只有是负数的情况下,“- scrollX”和“- scrollY” 才是正数,这下大家明白scrollTo()和scrollBy()为什么要传入负数了吧。

填坑2.为什么调用父布局的scrollTo和ScrollBy方法而不是子布局的呢?

一定要注意,不管是scrollTo()还是scrollBy()方法,滚动的都是该View内部的内容,而LinearLayout中的内容就是我们的两个Button。

2.3 computeScroll()方法讲解

这个方法也是View中的方法,而且这个方法也是比较重要的,看一下View类中的源码:

 /**
     * 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() {
    }

是的,你没看错,computeScroll()就是一个空方法!你可以有疑问空方法重要个毛啊, 这个方法需要我们自己来实现。

我们知道scrollTo()和scrollBy()会强制view进行绘制,也就是调用draw()方法,在draw方法中会调用这个computeScroll()方法。下面我们来看一下draw()方法:

 boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
            ...中间的很多代码(略)...

        if (!drawingWithRenderNode) {
            computeScroll();
            sx = mScrollX;
            sy = mScrollY;
        }

        ...其中很多代码(略)...

        return more;
    }

看到没,在draw方法中调用了computeScroll()方法。好讲到这里,我们用图来表示它们的流程,如下图:

通过这个图我相信大家都看出computeScroll()作用了:

通过在computeScroll()判断是否滑动结束,如果没有结束则继续滑动。

那么如何判断是否滑动结束呢?这就要用来我们下面讲的Scroller类了。

2.4小结

好了,费了这么大的篇幅终于介绍了完了View关于滑动的相关方法,下面我们就用这些方法并结合Scroller来做一个文章开头的动画效果。

3.Scroller类讲解

终于讲到这个类了,哎呀,写了一天了,累死我了。

3.1 Scroller类常用方法和属性介绍

属性:

startX 滚动起始点X坐标
startY 滚动起始点Y坐标
velocityX   当滑动屏幕时X方向初速度,以每秒像素数计算
velocityY   当滑动屏幕时Y方向初速度,以每秒像素数计算
minX    X方向的最小值,scroller不会滚过此点。
maxX    X方向的最大值,scroller不会滚过此点。
minY    Y方向的最小值,scroller不会滚过此点。
maxY    Y方向的最大值,scroller不会滚过此点。

方法:

1.computeScrollOffset()//判断是否已经滑动结束,true是没有结束,false已经结束。
2.startScroll(int startX, int startY, int dx, int dy, int duration)
3.startScroll(int startX, int startY, int dx, int dy)//以提供的起始点和将要滑动的距离开始滚动。滚动会使用缺省值250ms作为持续时间。startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间。(注意,这里只有记录信息的作用,真正滑动的不是它,而是ScrollTo和ScrollBy方法。)
4.mScroller.getCurrX() //获取mScroller当前水平滚动的位置  
5.mScroller.getCurrY() //获取mScroller当前竖直滚动的位置  
6.mScroller.getFinalX() //获取mScroller最终停止的水平位置  
7.mScroller.getFinalY() //获取mScroller最终停止的竖直位置  
8.mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置  
9.mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置

3.2用法总结:

总的来说Scroller类的用法分3步:

1、创建一个Scroller对象,一般在View的构造器中创建:
public MyStudyScroller(Context context, AttributeSet attrs) {
    super(context, attrs);
    scroller = new Scroller(context);
}
2、重写View的computeScroll()方法,一般来说下面的代码就是固定这么写,不会变。(注意是一般情况下):
@Override
public void computeScroll() {
            super.computeScroll();
            if (mScroller.computeScrollOffset()) {//是否滑动结束
                        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());//调用scrollTo()方法
                        postInvalidate();//强制重绘
                }
}
3、调用startScroll()方法,startX和startY为开始滚动的坐标点,dx和dy为对应的偏移量,during是滑动时间(也可省略,使用默认时间):
        mScroller.startScroll (int startX, int startY, int dx, int dy,int during);
invalidate();//强制重绘

3.3用Scroller实现一个效果

看一下activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
        <Button
            android:id="@+id/button"
            android:text="Button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
             />
        <mystudy.czh.com.myapp.MyStudyScroller
            android:id="@+id/myStudyScroller"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World!" />

        </mystudy.czh.com.myapp.MyStudyScroller>
    </LinearLayout>

看一下MainActivity.java

    public class MainActivity extends AppCompatActivity implements View.OnClickListener{
        private Button button;
        private MyStudyScroller myStudyScroller;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            findView();
        }
        private void findView(){
            button = (Button)findViewById(R.id.button);
            button.setOnClickListener(this);
            myStudyScroller  = (MyStudyScroller)findViewById(R.id.myStudyScroller);
        }

        @Override
        public void onClick(View v) {
            switch(v.getId()){
                case R.id.button:
                    myStudyScroller.scrollSmouthBy(-60,-300);
                    break;
            }
        }
    }

看一下MyStudyScroller.java

    public class MyStudyScroller extends LinearLayout {
    private Scroller scroller;

    public MyStudyScroller(Context context, AttributeSet attrs) {
        super(context, attrs);
        scroller = new Scroller(context);
    }

    public void scrollSmouthTo(int x, int y) {
        scroller.startScroll(scroller.getFinalX(), scroller.getFinalY(),  x, y, 4000);
        invalidate();
    }

    public void scrollSmouthBy(int x, int y) {
        scrollSmouthTo( x,  y);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        Log.d("computeScroll","x:"+scroller.getCurrX()+"   y:"+scroller.getCurrY());
        if (scroller.computeScrollOffset()) {//如果没有滑动结束,就继续滑动。
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            invalidate();
        }
    }
}

MyStudyScroller代码很简单,现在再回顾一下我们说的这3步:

1、创建一个Scroller对象,一般在View的构造器中创建。
2、重写View的computeScroll()方法,一般来说下面的代码就是固定这么写,不会变。
3、调用startScroll()方法,startX和startY为开始滚动的坐标点,dx和dy为对应的偏移量,during是滑动时间
invalidate();//强制重绘

运行效果图这里就不贴了,相信在文章开头大家都看了。

3.3用Scroller实现的整个流程图

结合上面的流程图,我们添加了Scroller之后的流程图如下:

3.4总结

经过上面的解释,我觉的你可能还未了解(因为我学的时候也这样),下面对一些疑问做一些解答:

1.Android ViewGroup中的Scroller与computeScroll的有什么关系?
答:没有直接的关系

2.Scroller到底是什么?
答:Scroller只是个计算器,提供插值计算,让滚动过程具有动画属性,但它并不是UI,也不是滑动辅助UI运动,反而是单纯地为滑动提供计算和保存相关的偏移值。无论从构造方法还是其他方法,以及Scroller的属性可知,其并不会持有View。

3.computeScroll和Scroller都是计算,两者有啥关系?

4.滑动时连续的,如何让Scroller的计算也是连续的?
这个就问到了什么时候调用computeScroll了,如第3条所说computeScroll调用scrollTo和scrollBy从而调用Scroller.startScroll()方法,只要computeScroll调用连续,Scroller也会连续,实质上computeScroll的连续性又invalidate方法控制,scrollTo,scrollBy都会调用invalidate,而invalidate回去触发draw,从而draw方法又调用computeScroll,以上所述表明computeScroll被连续调用,Scroller也会被连续调用,除非invalidate停止调用。
您可以在computeScroll()方法中加上一个打印,看看它的打印:
Log.d("computeScroll","x:"+scroller.getCurrX()+"   y:"+scroller.getCurrY());
事实上它一次滑动,computeScroll()方法调用了很多次。坐标都在是持续的变化。

5.computeScroll如何和Scroller的调用过程保持一致。
看看第4条吧。

所以说总的来说Scroller就是为了保存计算值而生的,它显的有些被动。

4.结尾

好的,那么本篇文章就到这里,希望对大家有所帮助,Scroller能实现很多效果,大家要学会它。

在技术上我依旧是个小渣渣,加油,勉励自己!

5.参考文档

1.Android——源码角度分析View的scrollBy()和scrollTo()的参数正负问题

2.Android Scroller完全解析,关于Scroller你所需知道的一切

3.详解Android Scroller与computeScroll的调用机制关系

4.Android 带你从源码的角度解析Scroller的滚动实现原理

5.深入理解Android中Scroller的滚动原理

6.Android 滚动操作Scroller类详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序编织梦想

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值