记得读到过一句话,大意为人为错误主要是由于工具的设计不合理导致的,一个设计良好的工具应该只需要少量的文档,或者根本不需要文档。SDK就是我们开发人员手中的工具,如果按照这句话来说的话,Android在很多方面设计的不够合理,因为很多类你即使认真读了文档(Android的文档也有待改进和完善),用起来也不是得心应手。个人认为Scroller就是一个例子。在具体说如何实现滚动之前,我们先来谈谈滚动的实质是什么。
滚动包含两种情形,一种是用户手指始终与屏幕接触,并在屏幕上滑动,此时View随着手指的滑动而滚动;另一种是用户快速的在屏幕上滑动后,手指离开屏幕,而View在手指离开屏幕后仍以一定的初始速度滚动,直到速度降为0,或者滚动到了view的两端。因此滚动可以实际上就是,View中的点在一定时间内的运动,本质上可以说是一定时间内由于View位置的改变而产生的一段动画
。既然是动画,那么就包含以下要素:
- 动画的时间
- 动画初始状态
- 动画中每一帧的状态
既然滚动也是动画,那么自然也需要设置上述状态,并需要根据动画中的每一帧来更新View。
####自定义View滚动的实现####
Scroller与OverScroller是Android提供的用来实现滚动效果的类,类中包装好了滚动的速度,时间,以及相关的差值方法。这两个类的接口非常相似,我们下面以OverScroller为例来说明, OverScroller的主要接口如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
*this will startScroll from point (startX, startY) by dx
*and dy respectively in x and y
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration)
/**
*Start scrolling based on a fling gesture. The distance traveled will
* depend on the initial velocity of the fling.
*/
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY, int overX, int overY)
/**
* Call this when you want to know the new location. If it returns true, the
* animation is not yet finished.
*/
public boolean computeScrollOffset()
OverScroller的接口就对应了我们上述的动画的不同要素:
- startScroll()函数接受滚动的起始位置,在x和y方向上需要滚动的距离,以及滚动动画的时间;fling()除了起始位置(x,y)外需要更多的参数,包括x和y方向上的速度,以及x和y方向上最大和最小滚动的距离等。这两个函数就是我们设置滚动初始状态的函数
- computeScrollOffset()函数利用差值函数与当前的时间点,计算出当前时刻动画的状态
有了滚动的初始状态以及滚动过程中每一帧的状态,我们还需要一个循环,使得我们能够在整个动画过程中获得动画的当前状态,并作用到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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57private class ScrollRunnable implements Runnable {
private OverScroller mScroller;
private int mCurrScrollX;
private int mCurrScrollY;
private boolean isScrolling;
//Constructor
public ScrollRunnable() {
LinearInterpolator linearInterpolator = new LinearInterpolator();
DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator();
//Create scroller and assign a interpolator to it.
mScroller = new OverScroller(mContext, decelerateInterpolator);
}
public void stopScroller() {
mScroller.abortAnimation();
isScrolling = false;
}
public void run() {
//If we are still scrolling get the new x,y values.
if(mScroller.computeScrollOffset()) {
float oldScrollX = mCurrScrollX;
float oldScrollY = mCurrScrollY;
mCurrScrollX = mScroller.getCurrX();
mCurrScrollY = mScroller.getCurrY();
//here we scroll our view, scrollBy is a method of View, this is part code of my project, and
//my View can only scroll horizontally, so dx is always 0
scrollBy((int)(mCurrScrollX - oldScrollX), (int)(0));
//If we have not reached our scroll limit,then run this Runnable again on UI event thread.
//thus we have a loop for the scroll
post(this);
}
}
//you can use gesture detector to detect fling gesture, and call this method in onFling.
public boolean onContentFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (isScrolling)
stopScroller();
isScrolling = true;
mScroller.fling(mCurrScrollX, mCurrScrollY, (int)-velocityX, (int)-velocityY,
0, HorizontalGallyView.this.getWidth(), 0, 0);
post(this);
return true;
}
////you can use gesture detector to detect scroll gesture, and call this method in onScroll.
public boolean onContentScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (isScrolling)
stopScroller();
isScrolling = true;
//start the scroll from the last position.
mScroller.startScroll(getScrollX(), 0, (int)distanceX, 0, 10);
post(this);
return true;
}
}
基本上是用上面的代码就可以实现滚动的效果。
####对OverScroller类设计的一些看法####
开始说过,我认为Android当中OverScroller类的实现不能说是好的设计,因为如果我们仅仅看文档而不看OverScroller类的源代码,很难知道正确的使用这个类的方法。因为要想正确使用,我们必须自己去维护一个Loop,并主动查询滚动过程中每一帧的状态,然后将状态apply到View上。
如果OverScroller采用了类似Property Animation的框架,应该会变得更易于使用。在Property Animation框架中,animator类会主动的将动画每一帧的结果通过OnAnimationUpdate传递给Listener,然后Listener可以更新动画的过程。这个框架的好处,就是将动画的整个loop包装在了动画的内部,我们只需要调用start()方法开始动画,之后只需被动利用监视器检测动画的过程即可。如果你不熟悉Property Animation,请看这里。