浅谈Android View滑动和弹性滑动
引言
View的滑动这一块在实际开发中是非常重要的,无论是优秀的用户体验还是自定义控件都是需要对这一块了解的,我们今天来谈一下View的滑动。
View的滑动
View滑动功能主要可以使用3种方式来实现;第一种是通过View的scrollTo/scrollBy方法来实现滑动。第二种是通过动画给View添加平移效果来实现滑动。第三种就是通过修改View的LayoutParams来实现View的滑动。下面我们来依次介绍。
scrollTo/scrollBy方法
为了实现View的滑动,View类实现了scrollTo和scrollBy方法,我们先来看一下这两个方法的实现。代码如下:
1 /** 2 * Set the scrolled position of your view. This will cause a call to 3 * {@link #onScrollChanged(int, int, int, int)} and the view will be 4 * invalidated. 5 * @param x the x position to scroll to 6 * @param y the y position to scroll to 7 */ 8 public void scrollTo(int x, int y) { 9 if (mScrollX != x || mScrollY != y) { 10 int oldX = mScrollX; 11 int oldY = mScrollY; 12 mScrollX = x; 13 mScrollY = y; 14 invalidateParentCaches(); 15 onScrollChanged(mScrollX, mScrollY, oldX, oldY); 16 if (!awakenScrollBars()) { 17 postInvalidateOnAnimation(); 18 } 19 } 20 } 21 22 /** 23 * Move the scrolled position of your view. This will cause a call to 24 * {@link #onScrollChanged(int, int, int, int)} and the view will be 25 * invalidated. 26 * @param x the amount of pixels to scroll by horizontally 27 * @param y the amount of pixels to scroll by vertically 28 */ 29 public void scrollBy(int x, int y) { 30 scrollTo(mScrollX + x, mScrollY + y); 31 }
我们看到scrollBy内部也是调用了scrollTo方法。从代码注释中我们也可以知道两者之间的区别。详细区别如下:
scrollTo的两个参数x,y表示的移动的具体位置(目标位置x,y),scrollTo实现了基于传递参数的绝对滑动。
scrollBy的两个参数x,y表示移动的偏移量,scrollBy实现了基于传递参数的相对滑动。
在学习View的滑动过程中,我们需要了解View内部mScrollX和mScrollY两个属性的变化规则,这两个属性可以通过getScrollX和getScrollY两个方法分别获得。
mScrollX的值总是等于View左边缘和View内容左边缘在水平方向上的距离。mSxrollY的值总是等于View上边缘和View内容上边缘在竖直方向上的距离。View的边缘是指View的位置,由四个顶点组成,而View的内容边缘是指View中内容的边缘,scrollTo和scrollBy方法只能改变View内容的位置,不能改变View在布局中的位置。
下面我们通过图来形象的看一下mScrollX和mScrollY在滑动过程中的变化过程。如图所示:
从图中我们可以看到,当View的左边缘在View内容左边缘的右边时(图中第一排第二个),mScrollX是正值,反之为负值。当View上边缘在View内容上边缘的下面时,mScrollY是正值,反之为负值。我们看到这和我们正常理解的向右侧滑动为正值的概念是相反的。这一点需要注意!
重点:
在追问下,上面说scrollTo改变的是View内容的位置,而不是View在布局中的位置,这个到底怎么整的?其实Android系统是通过移动可视区域从而达到改变View内容位置的。看看下面这张图:
例如我们移动图中黄色的View向右,我们自觉肯定是偏移量为正(X轴正方向为右边),但是我们在使用scrollBy的时候却要使用负值,这和我们自觉相反?如果Android实现滑动移动的是坐标系统呢?View要向右侧滑动,不是只要将坐标系统往左侧移动吗?不就是负值了。。。。。重点部分啊!
使用动画
上面我们介绍了通过View自有的scrollTo和scrollBy方法来实现滑动,现在我们来介绍使用动画来实现View的滑动。通过动画来实现View的滑动,主要就是操作View的translationX和translationY属性,通过修改这两个属性我们可以实现View的动画,这两个属性不明白?推荐看上一篇博客《View的定位》。
使用动画来实现View的滑动,可以使用传统的View动画也可以使用Android 3.0以后推出的属性动画(为了兼容Android 3.0以下的设备可以使用nineoldandroids动画兼容库)。
使用View动画需要注意一点,View动画是对View的影像做操作,它并不能真正的改变View的位置参数,包括宽度和高度,并且希望动画结束后的状态得以保持必须设置fillAfter为true,否则动画完成后其动画效果会消失。这一缺陷也会导致有事件处理的View使用View动画会出现更加严重的问题。试想如果一个View有事件处理代码(OnClick等),将其移动200px后,点击新位置的View是无法触发事件的,但是点击原来的位置居然可以触发事件,这就有些奇葩了。。。。。后面讲View动画的也会重点讲这一块。
结合上面的内容,我们推荐使用属性动画来实现绝大部分的动画效果,View动画建议只使用在没有事件处理的效果中。
改变布局参数
第三种实现View滑动的方法就是修改布局参数(即LayoutParams)。例如我们需要把一个View向右平移100px,我们只需要将这个View的LayoutParams里面的marginLeft参数的值添加100px即可。这种方式我在后面的博客中会详细介绍,这个一般和ValueAnimator配合使用的比较多,在动画执行过程中,不断修改LayoutParams的值,实现滑动效果。
三种滑动实现方式的异同点
scrollTo/scrollBy这种实现方式:操作简单,适合对View内容的滑动,也适合一些ViewGroup的滑动效果效果的实现。
动画实现方式:操作简单,View动画适用于没有交互的View。属性动画可以实现较为复杂的动画效果。
改变布局参数:操作稍微复杂,主要适用于有交互的View。
弹性滑动
下面我们开始介绍这一部分的重点内容,View的弹性滑动。实现View的弹性滑动也有好几种方法,下面就来一一介绍。
使用Scroller
我们在使用Scroller实现弹性滑动的时候有一套固定的方式来使用,固定方式代码如下:
1 /** 2 * 缓慢滚动到指定位置 3 * @param dx 4 */ 5 private void smoothScrollByDx(int dx) { 6 //在1000毫秒内滑动dx距离,效果就是慢慢滑动 7 mScroller.startScroll(getScrollX(), 0, dx, 0, 1000); 8 invalidate(); 9 } 10 11 @Override 12 public void computeScroll() { 13 if (mScroller.computeScrollOffset()) { 14 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 15 postInvalidate(); 16 } 17 }
上面的代码片段就是使用Scroller实现弹性滑动的典型方式,我们下面来简单的描述下其工作原理。我们看一下startScroll方法内部的逻辑,代码如下:
1 /** 2 * Start scrolling by providing a starting point, the distance to travel, 3 * and the duration of the scroll. 4 * 5 * @param startX Starting horizontal scroll offset in pixels. Positive 6 * numbers will scroll the content to the left. 7 * @param startY Starting vertical scroll offset in pixels. Positive numbers 8 * will scroll the content up. 9 * @param dx Horizontal distance to travel. Positive numbers will scroll the 10 * content to the left. 11 * @param dy Vertical distance to travel. Positive numbers will scroll the 12 * content up. 13 * @param duration Duration of the scroll in milliseconds. 14 */ 15 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 16 mMode = SCROLL_MODE; 17 mFinished = false; 18 mDuration = duration; 19 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 20 mStartX = startX; 21 mStartY = startY; 22 mFinalX = startX + dx; 23 mFinalY = startY + dy; 24 mDeltaX = dx; 25 mDeltaY = dy; 26 mDurationReciprocal = 1.0f / (float) mDuration; 27 }
我们看到startScroll方法内部其实什么都没做,只是保存了我们传递的几个参数。从传递的参数中我们可以看到,startX和startY表示的是滑动的起点,dx和dy表示的是滑动的距离,而duration表示的是滑动时间(整个滑动过程完成的时间),这里的滑动是指View内容的滑动而非View本身位置的改变。
注意点:
从上面的代码中我们看到startScroll是无法实现View的滑动的,它内部只是做了简单的赋值操作。是invalidate方法导致了View的重绘,在View的draw方法中又回去调用computeScroll方法(computeScroll方法在View中是一个空实现,需要我们自己去实现,我们上面的代码片段已经实现了这个方法)
具体过程如下:
当View重绘后会在draw方法中调用computeScroll,在computeScroll方法中会去向Scroller获取当前的scrollX和scrollY,然后通过scrollTo方法实现滑动。接着又调用postInvalidate方法来进行第二次重绘,这一次的重绘过程和第一次重绘一模一样,如此反复,直到整个滑动过程结束。
我们再看一下computeScollOffset方法的逻辑,代码如下:
1 public boolean computeScrollOffset() { 2 // 如果已经结束,直接返回false. 3 if (mFinished) { 4 return false; 5 } 6 7 // 计算已经度过的时间. 8 int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); 9 10 if (timePassed < mDuration) { 11 switch (mMode) { 12 // 处理滚动模式 13 case SCROLL_MODE: 14 // 根据过度的时间计算偏移比例 15 final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); 16 mCurrX = mStartX + Math.round(x * mDeltaX); 17 mCurrY = mStartY + Math.round(x * mDeltaY); 18 break; 19 // 处理fling模式...... 20 } 21 } else { 22 // 当时间结束时,直接将x和y坐标置为终止状态的x和y坐标,同时将终止标志位置为true. 23 mCurrX = mFinalX; 24 mCurrY = mFinalY; 25 mFinished = true; 26 } 27 return true;
上面代码就是根据时间流逝来计算当前scrollX和scrollY的值,大意是根据时间的流逝的百分比来计算scrollX和scrollY,这个类似动画中的插值器。这个方法返回true表示滑动还未结束,false表示滑动已经结束。
通过动画实现弹性滑动
动画本身就是一种渐进的过程,因此通过动画实现的滑动天生就具有弹性效果。我们通常情况下可以使用属性动画来实现。不过我们也可以使用动画来模拟实现Scroller的弹性滑动的效果,如图:
在这个代码中我们看到,我们动画本质上没有作用于任何对象上,我们利用动画的每一帧的到来时获取动画完成的比例,然后计算需要滑动的距离,这和Scroller的思想比较类似。
使用延时策略
这种方式的核心思想是通过发生一系列的延时消息从而实现一种渐进式的效果,具体来说是使用Handler和View的postDelayed方法。具体实现可以看下一篇文章《Android弹性滑动的实现方式》。