老实说这个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的调用机制关系