实现滑动效果的方式
在Android中有三种方式可以实现View的滑动效果:
1、通过View本身提供的
scrollTo(int x, int y)或者scrollBy(int x, int y)方法来实现View中内容滑动的效果。(内容滑动)
2、通过动画(最好使用属性动画)给View施加平移效果来实现滑动。(View滑动)
3、通过改变View的LayoutParams使得View重新布局来实现滑动效果。
三者对比:
1、scrollTo/scrollBy:操作简单适合对View内容的滑动。
2、动画:操作简单,主要适合没有交互的View和实现复杂的动画效果。
3、改变布局参数:操作稍微复杂,适用于有交互的View。
scroll滑动原理
在此处主要分析scroll方式的滑动。
- 首先,弄清楚两个变量的概念:mScrollX和mScrollY
源码
<span style="color:#009900;">/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")</span>
protected int mScrollX;
<span style="color:#009900;">/**
* The offset, in pixels, by which the content of this view is scrolled
* vertically.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")</span>
protected int mScrollY;
mScrollX和mScrollY的变化规律:
(1).mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离(mScrollX=X1-X2,,其中X1,表示View的左边缘,其中X2,表示View内容的左边缘),当View内容的左边缘位于View的左边缘的左边时,mScrollX大于零,即mScrollX为正值,反之为负值;
(2).mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离(mScrollY=Y1-Y2,,其中Y1,表示View的上边缘,其中Y2,表示View内容的上边缘),当View内容的上边缘位于View的上边缘的上边时,mScrollY大于零,即mScrollY为正值,反之为负值;
默认情况下,mScrollX和mScrollY都等于0(我的理解是在刚进入程序,scroll之前mScrollX=0和mScrollY=0)。
- scrollTo(int x,int y)的源码:
<span style="color:#009900;">/**
* 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
*/</span>
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();
}
}
}
在源码中,该方法是将View的内容移动到指定位置,可以看出每次
scrollTo(int x,int y)时,都会进行if (mScrollX!= x || mScrollY!= y)判断,检查目的点的坐标是否和偏移量一样,因为 scrollTo()是移动到指定的点,如果这次移动的点的坐标和上次偏移量一样,也就是说这次移动和上次移动的坐标是同一个,那么就没有必要进行移动了。
效果图:
上面几幅图就是,通过scrollTo进行上下左右移动后的效果,如果仔细看就会发现为什么当参数的正负与坐标系的正负相反呢?等会儿介绍方向问题。
- scrollBy(int x,int y)的源码
/**
* 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(int x,int y)的本质与
scrollTo(int x,int y)是一致的,
scrollTo(int x,int y)是从当前偏移量移动到指定点,而
scrollBy(int x,int y)是在当前偏移量的基础上根据参数提供的偏移量移动,所以
在参数不变的情况下,scrollTo(int x,int y)只能移动一次,而
scrollBy(int x,int y)可以一直地移动下去。
- scrollTo(int x,int y)移动的方向
在源码中,当滑动时会经过
invalidate(left, top, right, bottom)方法,而在这个方法中有
tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY)这步操作,所以当 scrollTo(int x,int y)的参数为正数时,绘制内容的区域会向坐标反方向移动。
所以scrollTo的移动方向:
弹性滑动
由于 scrollTo(int x,int y)滑动时,会瞬间移动,实际的体验效果不好,所以我们要实现渐进式滑动。渐进式滑动的思想是:将一次大的滑动分成若干次小的滑动,并在一个时间段内完成。
能够实现弹性滑动的方式很多,如通过Scroller、动画、(Handler+postDelay)或(Thread+sleep)等。
在此处使用Scroller实现。
首先,介绍Scroller的几个常用方法:
- startScroll(int startX, int startY, int dx, int dy, int duration)
从方法名字来看应该是滑动开始的地方,事实上我们在使用的时候也是先调用这个方法的,该方法的主要作用是:一个构造方法用来初始化赋值的,比如设置滚动模式、开始时间,持续时间、起始坐标、结束坐标等等,并没有任何对View的滚动操作。
源码:
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
源码中验证了,上述结论。使用Scroller进行滑动时,在startScroll(int startX, int startY, int dx, int dy, int duration),方法中只是进行了初始化。
- computeScrollOffset()
方法主要是根据当前已经消逝的时间来计算当前的坐标点,并且保存在mCurrX和mCurrY值中。之前在startScroll()方法的时候获取了当前的动画毫秒并赋值给了mStartTime,在computeScrollOffset()中再一次调用AnimationUtils.currentAnimationTimeMillis()来获取动画毫秒减去mStartTime就是消逝时间了。然后进去if判断,如果动画持续时间小于设置的滚动持续时间mDuration,则是SCROLL_MODE,再根据Interpolator来计算出在该时间段里面移动的距离,移动的距离是根据这个消逝时间乘以mDurationReciprocal,就得到一个相对偏移量,再进行Math.round(x * mDeltaX)计算,就得到最后的偏移量,然后赋值给mCurrX, mCurrY,所以mCurrX、 mCurrY 的值也是一直变化的。总结一下该方法的作用就是,计算在0到mDuration时间段内滚动的偏移量,并且判断滚动是否结束,true代表还没结束,false则表示滚动结束了。
Scroller实现弹性滑动的流程:
首先是View通过Scroller的 startScroll(int startX, int startY, int dx, int dy, int duration)方法进行初始化。
然后,会调用View的
invalidate()或postInvalidate()进行重绘。
接着,绘制View的时候会触发computeScroll()方法,接着重写computeScroll(),在computeScroll()里面先调用Scroller的computeScrollOffset()方法来判断滚动是否结束,如果滚动没有结束就调用scrollTo()方法来进行滚动。
最后,scrollTo()方法虽然会重新绘制View,但是还是要手动调用下invalidate()或者postInvalidate()来触发界面重绘,重新绘制View又触发computeScroll(),所以就进入一个递归循环阶段,这样就实现在某个时间段里面滚动某段距离的一个平滑的滚动效果。