注: 本文提到的所有三种滑动方式的完整demo:ScrollDemo
1. 关于View我们需要知道的
(1)什么是View?
Android中的View类是所有UI控件的基类(Base class),也就是说我们平时所有到的各种UI控件,比如Button、ImagView等等都继承自View类。LinearLayout、FrameLayout等布局管理器的直接父类是ViewGroup,而ViewGroup也有View类派生。总的来说,View是对UI控件的抽象,它代表了屏幕上的一个矩形区域。通过继承View,并重写相应方法,我们就能够实现具有各种外观及行为的UI控件。Button等控件我们之所以能够直接拿来即用,是因为Google已经帮我们完成了继承View并重写方法的工作。
(2)View的位置
View在屏幕上的位置由它的以下四个参数所决定:
- top:View的左上角的纵坐标,对应着View类中的成员变量mTop,可由getTop方法获得;
- left:View的左上角的横坐标,对应着View类中的成员变量mLeft,可由getLeft方法获得;
- bottom:View的右下角的纵坐标,对应着View类中的成员变量mBottom,可由getBottom方法获得;
- right:View的右下角的横坐标,对应着View类中的成员变量mRight,可由getRight方法获得。
注意,以上的坐标都是相对于父View来说的,也就是说,坐标都是相对坐标,因为子View的布局是由父View来完成的。如下图所示:
有了这四个参数,计算View的宽高就很容易了:width = right - left;height = bottom - top。关于View还有两个参数需要我们注意:translationX代表View平移的水平距离,translationY代表View平移的竖直距离;x、y分别为View的左上角的横纵坐标。View若经过了平移,改变的是它的x、y(代表当前View的左上角位置),它的四个位置参数代表了View的原始位置信息,是始终不变的。View在平移的过程中始终满足如下关系:
x = left + translationX; y = top + translationY。
2. 实现View滑动的几种方式
我们在使用View的过程中,经常需要实现View的滑动效果。比如ListView、跟随手指而移动的自定义View等等,前者的滑动效果是SDK为我们提供的,而对于我们自定义View的滑动效果就需要我们自己来实现。下面我们来详细介绍以下实现View滑动的几种方式。
(1)使用scrollTo/scrollBy实现View的滑动
实现滑动的最朴素直接的方式就是使用View类自带的scrollTo/scrollBy方法了。scrollBy方法是滑动指定的位移量,而scrollTo方法是滑动到指定位置。这两个方法的源码如下:
1 /** 2 * Set the scrolled position of your view. This will cause a call to 3 4 * {@link #onScrollChanged(int, int, int, int)} and the view will be 5 6 * invalidated. 7 8 * @param x the x position to scroll to 9 10 * @param y the y position to scroll to 11 12 */ 13 public void scrollTo(int x, int y) { 14 if (mScrollX != x || mScrollY != y) { 15 int oldX = mScrollX; 16 int oldY = mScrollY; 17 mScrollX = x; 18 mScrollY = y; 19 onScrollChanged(mScrollX, mScrollY, oldX, oldY); 20 if (!awakenScrollBars()) { 21 invalidate(); 22 } 23 } 24 } 25 26 /** 27 * Move the scrolled position of your view. This will cause a call to 28 * {@link #onScrollChanged(int, int, int, int)} and the view will be 29 * invalidated. 30 * @param x the amount of pixels to scroll by horizontally 31 * @param y the amount of pixels to scroll by vertically 32 */ 33 public void scrollBy(int x, int y) { 34 scrollTo(mScrollX + x, mScrollY + y); 35 }
通过以上代码的33~35行我们可以看到,scrollBy方法内部也是调用了scrollTo方法来实现。以上源码中我们注意到了mScrollX和mScrollY成员变量,前者是View的左边缘减去View的内容的左边缘,后者是View的上边缘减去View的内容的上边缘。示意图如下:
上图中,黑色边框代表View在屏幕上对应的矩形区域,蓝色边框代表View的内容。在上图中,我们调用scrollTo/scrollBy把View向右滚动了一定距离。实际上,调用scrollBy/scrollTo方法只能实现View的内容的滚动,而View的四个位置参数是保持不变的。想一下我们平常使用ListView时,滚动的就是ListView的内容,而ListView本身在屏幕上的位置是不变的。上图中,黑色左边(即View的左边缘)减去蓝色左边(即View的内容的左边缘)即可得到mScrollX。由此我们还可以知道,向右滚动时mScrollX负的,向左滚动时mScrollX是正的。同理我们可以知道,向下滚动时,mScrollY是负的,向上滚动时,mScrollY是正的。
经过以上的分析,我们了解到使用scrollTo/scrollBy方法实现View的滑动是很简单直接的,那么简单的背后有什么代价呢?代价就是滑动不是“弹性的”,弹性滑动指的是View的滑动应该是一个先加速再逐渐减速到停止的过程,这样看起来很平滑,不会很突兀。scrollTo/scrollBy方法实现的滑动看起来就会很突兀,这样的用户体验很不好。在解决这个问题之前,我们先来看看实现View滑动的其他方法。
(2)使用动画来实现View的滑动
使用动画来实现View的滑动主要通过改变View的translationX和translationY参数来实现,使用动画的好处在于滑动效果是平滑的。上面我们提到过,View的x、y参数决定View的当前位置,通过改变translationX和translationY,我们就可以改变View的当前位置。我们可以使用属性动画或者补间动画来实现View的平移。
首先,我们先来看一下如何使用补间动画来实现View的平移。补间动画资源定义如下(anim.xml):
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true"> <translate android:duration="100" android:fromXDelta="0" android:fromYDelta="0" android:interpolator="@android:anim/linear_interpolator" android:toXDelta="100" android:toYDelta="100"/> </set>
然后在onCreat方法中调用startAnimation方法即可。使用补间动画实现View的滑动有一个缺陷,那就是移动的知识View的“影像”,这意味着其实View并未真正的移动,只是我们看起来它移动了而已。拿Button来举例,假若我们通过补间动画移动了一个Button,我们会发现,在Button的原来位置点击屏幕会出发点击事件,而在移动后的Button上点击不会触发点击事件。
接下来,我们看看如何用属性动画来实现View的平移。使用属性动画实现View的平移更加简单,只需要以下一条语句:
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
以上代码即实现了使用属性动画把targetView在100ms内向右平移100px。使用属性动画的限制在于真正的属性动画只可以在Android 3.0+使用(一些第三方库实现的兼容低版本的属性动画不是真正的属性动画),优点就是它可以真正的移动View而不是仅仅移动View的影像。
经过以上的描述,使用属性动画实现View的滑动看起来是个不错的选择,而且一些View的复杂的滑动效果只有通过动画才能比较方便的实现。
(3)通过改变布局参数来实现View的滑动
通过改变布局参数来实现View的滑动的思想很简单:比如向右移动一个View,只需要把它的marginLeft参数增大,向其它方向移动同理,只需改变相应的margin参数。还有一种比较拐弯抹角的方法是在要移动的View的旁边预先放一个View(初始宽高设为0)。然后比如我们要向右移动View,只需把预先放置的那个View的宽度增大,这样就把View“挤”到右边了。代码示例如下:
MarginLayoutParams params = (MarginLayoutParams) mButton.getLayoutParams(); params.leftMargin += 100; mButton.requestLayout();
以上代码即实现了把mButton向右滑动100px。通过改变布局参数来实现的滑动效果也不是平滑的。
(4)使用Scroller来实现弹性滑动
上面我们提到了使用scrollTo/scrollBy方法实现View的滑动效果不是平滑的,好消息是我们可以使用Scroller方法来辅助实现View的弹性滑动。使用Scroller实现弹性滑动的惯用代码如下:
1 Scroller scroller = new Scroller(mContext); 2 3 private void smoothScrollTo(int dstX, int dstY) { 4 int scrollX = getScrollX(); 5 int delta = dstX - scrollX; 6 scroller.startScroll(scrollX, 0, delta, 0, 1000); 7 invalidate(); 8 } 9 10 @Override 11 public void computeScroll() { 12 if (scroller.computeScrollOffset()) { 13 scrollTo(scroller.getCurrX(), scroller.getCurY()); 14 postInvalidate(); 15 } 16 }
我们来看一下以上的代码。第4行中,我们获取到View的mScrollX参数并存到scrollX变量中。然后在第5行计算要滑动的位移量。第6行调用了startScroll方法,我们来看看startScroll方法的源码:
1 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 2 mMode = SCROLL_MODE; 3 mFinished = false; 4 mDuration = duration; 5 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 6 mStartX = startX; 7 mStartY = startY; 8 mFinalX = startX + dx; 9 mFinalY = startY + dy; 10 mDeltaX = dx; 11 mDeltaY = dy; 12 mDurationReciprocal = 1.0f / (float) mDuration; 13 14 mViscousFluidScale = 8.0f; 15 16 mViscousFluidNormalize = 1.0f; 17 mViscousFluidNormalize = 1.0f / viscousFluid(1.0f); 18 }
从以上的源码我们可以看到,startScroll方法中并没有进行实际的滚动操作,而是把startX、startY、deltaX、deltaY等参数都保存了下来。那么究竟怎么实现View的滑动的呢?我们先回到Scroller惯用代码。我们看到第7行调用了invalidate方法,这个方法会请求重绘View,这会导致View的draw的方法被调用,draw的方法内部会调用computeScroll方法。我们来看看第13行,调用了scrollTo方法,并传入mScroller.getCurrX()和mScroller.getCurrY()方法作为参数。那么获取到的这两个参数是什么呢?这两个参数是在第12行调用的computeScrollOffset方法中设置的,我们来看看这个方法中设置这两个参数的相关代码:
1 public boolean computeScrollOffset() { 2 ... 3 int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); 4 if (timePassed < mDuration) { 5 switch (mMode) { 6 case SCROLL_MODE: 7 final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); 8 mCurrX = mStartX + Math.round(x * mDeltaX); 9 mCurrY = mStartY + Math.rounc(y * mDeltaY); 10 break; 11 ... 12 } 13 } 14 return true; 15 }
以上代码中第8行和第9行设置的mCurrX和mCurrY即为以上scrollTo的两个参数,表示本次滑动的目标位置。computeScrollOffset方法返回true表示滑动过程还未结束,否则表示结束。
通过以上的分析,我们大概了解了Scroller实现弹性滑动的原理:invaldate方法会导致View的draw方法被调用,而draw会调用computeScroll方法,因此重写了computeScroll方法,而computeScrollOffset方法会根据时间的流逝动态的计算出很小的一段时间应该滑动多少距离。也就是把一次滑动拆分成无数次小距离滑动从而实现“弹性滑动”。
3. 参考资料
《Android开发艺术探索》
以上叙述中若有不清晰或是不准确的地方,希望大家指出:)