scroller

Scroller的总结
内容来自各个博客

  • 本质及作用:
    Scroller本质就是一个Helper类,里面保存了目标对象要移动的距离,时间等属性!

Android ViewGroup中的Scroller与computeScroll的有什么关系?

答:没有直接的关系

1.Scroller到底是什么?

答:Scroller只是个计算器,提供插值计算,让滚动过程具有动画属性,但它并不是UI,也不是辅助UI滑动,反而是单纯地为滑动提供计算。

无论从构造方法还是其他方法,以及Scroller的属性可知,其并不会持有View,辅助ViewGroup滑动

2.Scroller只是提供计算,那谁来调用computeScroll使得ViewGroup滑动

答:computeScroll也不是来让ViewGroup滑动的,真正让ViewGroup滑动的是scrollTo,scrollBy。computeScroll的作用是计算ViewGroup如何滑动。而computeScroll是通过draw来调用的。

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

答:文章开始已作答,没有直接的关系。computeScroll和Scroller要是飞得拉关系的话,那就是computeScroll可以参考Scroller计算结果来影响scrollTo,scrollBy,从而使得滑动发生改变。也就是Scroller不会调用computeScroll,反而是computeScroll调用Scroller。

4.滑动时连续的,如何让Scroller的计算也是连续的?

这个就问到了什么时候调用computeScroll了,如上所说computeScroll调用Scroller,只要computeScroll调用连续,Scroller也会连续,实质上computeScroll的连续性又invalidate方法控制,scrollTo,scrollBy都会调用invalidate,而invalidate回去触发draw,从而computeScroll被连续调用,综上,Scroller也会被连续调用,除非invalidate停止调用。

5.computeScroll如何和Scroller的调用过程保持一致。

computeScroll参考Scroller影响scrollTo,scrollBy,实质上,为了不重复影响scrollTo,scrollBy,那么Scroller必须终止计算currX,currY。要知道计算有没有终止,需要通过mScroller.computeScrollOffset()

1
2
3
4
5
6
7
8
9
10
11
@Override
public void computeScroll(){
super.computeScroll();
if(!mScroll.computeScrollOffset()){
//计算currX,currY,并检测是否已完成"滚动"
return;
}
int tempX=mScroll.getCurrX();
scrollTo(tempX,0);//会重复调用invalidate
}

注意:在移动平台中,要明确知道“滑动”与“滚动”的不同,具体来说,滑动和滚动的方向总是相反的。

再来看一下scrollTo,scrollBy这两个方法的区别, scrollTo()方法是让View相对于初始的位置滚动某段距离,由于View的初始位置是不变的,因此不管我们点击多少次scrollTo按钮滚动到的都将是同一个位置。而scrollBy()方法则是让View相对于当前的位置滚动某段距离,那每当我们点击一次scrollBy按钮,View的当前位置都进行了变动,因此不停点击会一直移动。

通过这个例子来理解,相信大家已经把scrollTo()和scrollBy()这两个方法的区别搞清楚了,但是现在还有一个问题,目前使用这两个方法完成的滚动效果是跳跃式的,没有任何平滑滚动的效果。没错,只靠scrollTo()和scrollBy()这两个方法是很难完成ViewPager这样的效果的,因此我们还需要借助另外一个关键性的工具,也就我们今天的主角Scroller。
Scroller的基本用法其实还是比较简单的,主要可以分为以下几个步骤:

1.创建Scroller的实例
2.调用startScroll()方法来初始化滚动数据并刷新界面
3.重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
那么下面我们就按照上述的步骤,通过一个模仿ViewPager的简易例子来学习和理解一下Scroller的用法。
新建一个ScrollerLayout并让它继承自ViewGroup来作为我们的简易ViewPager布局,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/**
* Created by guolin on 16/1/12.
*/
public class ScrollerLayout extends ViewGroup {

/**
* 用于完成滚动操作的实例
*/
private Scroller mScroller;

/**
* 判定为拖动的最小移动像素数
*/
private int mTouchSlop;

/**
* 手机按下时的屏幕坐标
*/
private float mXDown;

/**
* 手机当时所处的屏幕坐标
*/
private float mXMove;

/**
* 上次触发ACTION_MOVE事件时的屏幕坐标
*/
private float mXLastMove;

/**
* 界面可滚动的左边界
*/
private int leftBorder;

/**
* 界面可滚动的右边界
*/
private int rightBorder;

public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// 第一步,创建Scroller的实例
mScroller = new Scroller(context);
ViewConfiguration configuration = ViewConfiguration.get(context);
// 获取TouchSlop值
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
}

@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);
// 为ScrollerLayout中的每一个子控件测量大小
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);
// 为ScrollerLayout中的每一个子控件在水平方向上进行布局
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
// 初始化左右边界值
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(getChildCount() - 1).getRight();
}
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();
mXLastMove = mXDown;
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
float diff = Math.abs(mXMove - mXDown);
mXLastMove = mXMove;
// 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
if (diff > mTouchSlop) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mXMove = event.getRawX();
int scrolledX = (int) (mXLastMove - mXMove);
if (getScrollX() + scrolledX < leftBorder) {
scrollTo(leftBorder, 0);
return true;
} else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
scrollTo(rightBorder - getWidth(), 0);
return true;
}
scrollBy(scrolledX, 0);
mXLastMove = mXMove;
break;
case MotionEvent.ACTION_UP:
// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
// 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
}

@Override
public void computeScroll() {
// 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}

整个Scroller用法的代码都在这里了,代码并不长,一共才100多行,我们一点点来看。
首先在ScrollerLayout的构造函数里面我们进行了上述步骤中的第一步操作,即创建Scroller的实例,由于Scroller的实例只需创建一次,因此我们把它放到构造函数里面执行。另外在构建函数中我们还初始化的TouchSlop的值,这个值在后面将用于判断当前用户的操作是否是拖动。

接着重写onMeasure()方法和onLayout()方法,在onMeasure()方法中测量ScrollerLayout里的每一个子控件的大小,在onLayout()方法中为ScrollerLayout里的每一个子控件在水平方向上进行布局。

接着重写onInterceptTouchEvent()方法, 在这个方法中我们记录了用户手指按下时的X坐标位置,以及用户手指在屏幕上拖动时的X坐标位置,当两者之间的距离大于TouchSlop值时,就认为用户正在拖动布局,然后我们就将事件在这里拦截掉,阻止事件传递到子控件当中。

那么当我们把事件拦截掉之后,就会将事件交给ScrollerLayout的onTouchEvent()方法来处理。如果当前事件是ACTION_MOVE,说明用户正在拖动布局,那么我们就应该对布局内容进行滚动从而影响拖动事件,实现的方式就是使用我们刚刚所学的scrollBy()方法,用户拖动了多少这里就scrollBy多少。另外为了防止用户拖出边界这里还专门做了边界保护,当拖出边界时就调用scrollTo()方法来回到边界位置。

如果当前事件是ACTION_UP时,说明用户手指抬起来了,但是目前很有可能用户只是将布局拖动到了中间,我们不可能让布局就这么停留在中间的位置,因此接下来就需要借助Scroller来完成后续的滚动操作。首先这里我们先根据当前的滚动位置来计算布局应该继续滚动到哪一个子控件的页面,然后计算出距离该页面还需滚动多少距离。接下来我们就该进行上述步骤中的第二步操作,调用startScroll()方法来初始化滚动数据并调用invalidate()来刷新界面。startScroll()方法接收四个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。紧接着调用invalidate()方法来刷新界面。

现在前两步都已经完成了,最后我们还需要进行第三步操作,即重写computeScroll()方法,并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll()方法是会一直被调用的,因此我们需要不断调用Scroller的computeScrollOffset()方法来进行判断滚动操作是否已经完成了,如果还没完成的话,那就继续调用scrollTo()方法,并把Scroller的curX和curY坐标传入,然后刷新界面从而完成平滑滚动的操作。


实现惯性滚动 (Scroller的妙用)
Android实现圆弧滑动效果之ArcSlidingHelper篇
说到Scroller,相信大家第一时间想到要配合View中的computeScroll方法来使用对吧,但是呢,我们这篇文章的主题是辅助类,并不打算继承View,而且不持有Context引用,这个时候,可能有同学就会有以下疑问了:

  • 这种情况下,Scroller还能正常工作吗?
  • 调用它的startScroll或fling方法后,不是还要调用View中的invalidate方法来触发的吗?
  • 不继承View,哪来的 invalidate方法?
  • 不继承View,怎么重写computeScroll方法?在哪里处理惯性滚动?
  • 哈哈,其实Scroller是完全可以脱离View来使用的,既然说是妙用,妙在哪里呢?在开始之前,我们先来了解一下Scroller:

1.它看上去更像是一个ValueAnimator,但它跟ValueAnimator有个明显的区别就是:它不会主动更新动画的值。我们在获取最新值之前,总是要先调用computeScrollOffset方法来刷新内部的mCurrX、mCurrY的值,如果是惯性滚动模式(调用fling方法),还会刷新mCurrVelocity的值。

2.在这里先分享大家一个理解源码调用顺序的方法:
比如我们想知道是哪个方法调用了computeScroll:

1
2
3
4
5
6
7
8
@Override
public void computeScroll() {
StackTraceElement[] elements = Thread.currentThread().getStackTrace();
for (StackTraceElement element : elements) {
Log.i("computeScroll", String.format(Locale.getDefault(), "%s----->%s\tline: %d",
element.getClassName(), element.getMethodName(), element.getLineNumber()));
}
}

日志输出:

1
2
3
4
5
6
7
8
9
10
11
com.wuyr.testview.MyView----->computeScroll	line: 141
android.view.View----->updateDisplayListIfDirty line: 15361
android.view.View----->draw line: 16182
android.view.ViewGroup----->drawChild line: 3777
android.view.ViewGroup----->dispatchDraw line: 3567
android.view.View----->updateDisplayListIfDirty line: 15373
android.view.View----->draw line: 16182
android.view.ViewGroup----->drawChild line: 3777
android.view.ViewGroup----->dispatchDraw line: 3567
android.view.View----->updateDisplayListIfDirty line: 15373
android.view.View----->draw line: 16182

这样我们就能够很清晰的看到它的调用链了。

回到正题,所谓的调用invalidate方法来触发,是这样的:我们都知道,调用了这个方法之后,onDraw方法就会回调,而调用onDraw的那个方法,是draw(Canvas canvas),再上一级,是draw(Canvas canvas, ViewGroup parent, long drawingTime),重点来了:
computeScroll也是在这个方法中回调的,现在可以得出一个结论:
我们在View中调用invalidate方法,也就是间接地调用computeScroll,而computeScroll中,是我们处理滚动的方法,在使用Scroller时,我们都会重写这个方法,并在里面调用Scroller的computeScrollOffset方法,然后调用getCurrX或getCurrY来获取到最新的值。(好像我前面说的都是多余的) 但是!有没有发现,这个过程,我们完全可以不依赖View来做到的?

3.现在思路就很清晰了,invalidate方法?对于Scroller来说,它的作用只是回调computeScroll从而更新x和y的值而已。

4.所以完全可以自己写两个方法来实现Scroller在View中的效果,我们这次打算利用Hanlder来帮我们处理异步的问题,这样的话,我们就不用自己新开线程去不断的调用方法啦。

好了,现在我们所遇到的问题,都已经有解决方案了,可以动手咯!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
 /**
* 处理触摸事件
*/
public void handleMovement(MotionEvent event) {
.....
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
.....
break;
case MotionEvent.ACTION_MOVE:
handleActionMove(x, y);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
if (isInertialSlidingEnable) {
mVelocityTracker.computeCurrentVelocity(1000);
mScroller.fling(0, 0, (int) mVelocityTracker.getXVelocity(), (int) mVelocityTracker.getYVelocity(),Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
//我们在调用Scroller的fling方法之后,并没有调用invalidate方法,而是我们自定义的startFling方法.
startFling();
}
break;
.....
}
.....
}


/**
* 处理惯性滚动
*/
private void computeInertialSliding() {
checkIsRecycled();
if (mScroller.computeScrollOffset()) {
float y = ((isShouldBeGetY ? mScroller.getCurrY() : mScroller.getCurrX()) * mScrollAvailabilityRatio);
if (mLastScrollOffset != 0) {
float offset = fixAngle(Math.abs(y - mLastScrollOffset));
mSlidingListener.onSliding(isClockwiseScrolling ? offset : -offset);
}
mLastScrollOffset = y;
startFling();
} else if (mScroller.isFinished()) {
mLastScrollOffset = 0;
if (mSlideFinishListener != null) {
mSlideFinishListener.onSlideFinished();
}
}
}

/**
* 开始惯性滚动
*/
private void startFling() {
mHandler.sendEmptyMessage(0);
}

/**
* 主线程回调惯性滚动
*Handler来处理异步的问题,这样就不用自己去新开线程了。
*/
private static class InertialSlidingHandler extends Handler {

ArcSlidingHelper mHelper;

InertialSlidingHandler(ArcSlidingHelper helper) {
mHelper = helper;
}

@Override
public void handleMessage(Message msg) {
mHelper.computeInertialSliding();
}
}

我们用computeInertialSliding来代替了View中的computeScroll方法,用startFling代替了invalidate,可以说是完全脱离了View来使用Scroller,妙就妙在这里啦,嘻嘻。


Scroller主要使用的滚动方法有:startScroll、fling。

1
2
3
4
5
startScroll(int startX, int startY, int dx, int dy, int duration)
指定起点(startX,startY),从起点平滑变化(dx,dy),耗时duration,通常用于:知道起点与需要改变的距离的平滑滚动等。

fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY):
惯性滑动。 给定一个初始速度( velocityX, velocityY ),该方法内部会根据这个速度去计算需要滑动的距离以及需要耗费的时间。通常用于:界面的惯性滑动等。

scroller的实例化:

1
2
Scroller mScroller = new Scroller(Context mContext){}; :采用默认插值器
Scroller mScroller = new Scroller(Context mContext,Interpolator interpolator){};采用指定的插值器

调用过程:

   public void startScroll( int startX, int startY, int dx,int dy){};

这方法并不是真正意义上的开始Scroll,它的作用是为Scroller做一些准备工作,比如设置移动的初始位置,滑动的位移以及持续时间等。

   public boolean computeScrollOffset(){}

这方法用于判断移动过程是否完成

   getCurrX、getCurrY、getFinalX、getFinalY、

这些方法用于获取scroll的一些位置信息

  • Scroller与View结合使用:

首先需要在自定义的View中定义一个私有成员 mScroller,用于记录view滚动的位置,然后再重写View的computeScroll()方法来实现具体移动

注意:Scroller的作用只是保存一些信息,以及判断是否移动完成,所以我们得知道computeScroll()这个方法的调用流程,在查看Android源码时发现
View.java中的computeScroll()方法是一个空函数,所以我们需要在自定义的View中重写这个方法来实现我们想要的功能,那么computeScroll()是怎么样被调用的呢?

调用invalidate()(UI线程)或者postInvalidate()使View(Viewgroup)树重绘.
重绘分成两种情况:

1、Viewgroup的重绘
ViewGroup的绘制流程:onDraw()-->dispatchDraw()-->drawChild()-->child.computeScroll()

2、View的重绘:

View没有子view所以在View的源码中看到dispatchDraw()方法是一个空方法,那么其调用流程就和ViewGroup是不一样的,那么View是怎样调用computeScroll()的呢?

我们注意到invalidate是重绘整个View树或者ViewGroup树,所以当View重绘时其所在父容器也会重绘,so,父容器就会经历onDraw()-->dispatchDraw()-->drawChild() -->child.computeScroll()流程,这时候自定义View中重写的computeScroll()方法就会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 ViewGroup.java:

@Override
protected void dispatchDraw(Canvas canvas){

for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)

{
more |= drawChild(canvas, child, drawingTime);
}
drawChild函数:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

----------

child.computeScroll();

-----------------

if ((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
child.dispatchDraw(canvas);
} else {
child.draw(canvas);
}
  • 自定义View:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class CustomView extends View {  

private Scroller mScroller;

public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}

//调用此方法滚动到目标位置
public void smoothScrollTo(int fx, int fy) {
int dx = fx - mScroller.getFinalX();
int dy = fy - mScroller.getFinalY();
smoothScrollBy(dx, dy);
}

//调用此方法设置滚动的相对偏移
public void smoothScrollBy(int dx, int dy) {
//设置mScroller的滚动偏移量
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
}

@Override
public void computeScroll() {
//先判断mScroller滚动是否完成
if (mScroller.computeScrollOffset()) {
//这里调用View的scrollTo()完成实际的滚动
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());

//必须调用该方法,否则不一定能看到滚动效果
postInvalidate();
}
super.computeScroll();
}
}

android.view.VelocityTracker主要用跟踪触摸屏事件(flinging事件和其他gestures手势事件)的速率。用addMovement(MotionEvent)函数将Motion event加入到VelocityTracker类实例中.你可以使用getXVelocity() 或getXVelocity()获得横向和竖向的速率到速率时,但是使用它们之前请先调用computeCurrentVelocity(int)来初始化速率的单位 。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
private VelocityTracker mVelocityTracker;//生命变量 
//在onTouchEvent(MotionEvent ev)中
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();//获得VelocityTracker类实例
}
mVelocityTracker.addMovement(ev);//将事件加入到VelocityTracker类实例中
//判断当ev事件是MotionEvent.ACTION_UP时:计算速率
final VelocityTracker velocityTracker = mVelocityTracker;
// 1000 provides pixels per second
velocityTracker.computeCurrentVelocity(1, (float)0.01); //设置maxVelocity值为0.1时,速率大于0.01时,显示的速率都是0.01,速率小于0.01时,显示正常
Log.i("test","velocityTraker"+velocityTracker.getXVelocity());
velocityTracker.computeCurrentVelocity(1000); //设置units的值为1000,意思为一秒时间内运动了多少个像素
Log.i("test","velocityTraker"+velocityTracker.getXVelocity());

大体的使用是这样的:
当你需要跟踪触摸屏事件的速度的时候,使用obtain()方法来获得VelocityTracker类的一个实例对象
在onTouchEvent回调函数中,使用addMovement(MotionEvent)函数将当前的移动事件传递给VelocityTracker对象
使用computeCurrentVelocity (int units)函数来计算当前的速度,使用 getXVelocity ()、 getYVelocity ()函数来获得当前的速度


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!