详解Android中实现View滑动的几种方式

关于View我们需要知道的

什么是View

Android中的View类是所有UI控件的基类(base class),我们平时所有到的各种UI控件,比如Button、ImagView等都继承自View类。LinearLayout、FrameLayout等布局管理器的直接父类是ViewGroup,而ViewGroup也由View类派生。总的来说,View是对UI控件的抽象,它代表了屏幕上的一个矩形区域。通过继承View,并重写相应方法,我们就能够实现具有各种外观及行为的UI控件。Button等控件我们之所以能够直接拿来即用,是因为Google已经帮我们完成了继承View并重写相应方法的工作。

View的位置

View在屏幕上的位置由它的以下四个参数所决定:

  • top:View的上边缘与父View的上边缘的距离,对应着View类中的成员变量mTop,可由getTop()方法获得;
  • left:View的左边缘与父View的左边缘的距离,对应着View类中的成员变量mLeft,可由getLeft方法获得;
  • bottom:View的下边缘与父View的下边缘的距离,对应着View类中的成员变量mBottom,可由getBottom方法获得;
  • right:View的右边缘与父View的右边缘的距离,对应着View类中的成员变量mRight,可由getRight方法获得。

一图胜千言:


有了这四个参数,计算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

实现View滑动的几种方式

我们在使用View的过程中,经常需要实现View的滑动效果。比如ListView、跟随手指移动的自定义View等等,前者的滑动效果是SDK为我们提供的,而对于我们自定义View的滑动效果就需要自己来实现。下面来详细介绍一下实现View滑动的几种方式。

使用scrollTo/scrollBy实现View的滑动

实现滑动的最朴素直接的方式就是使用View类自带的scrollTo/scrollBy方法了。scrollBy方法是滑动指定的位移量,而scrollTo方法是滑动到指定位置。这两个方法的源码如下:

/** 
  * 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 
  */ 
public void scrollTo(int x, int y) {
  if (mScrollX != x || mScrollY != y) {
    int oldX = mScrollX; 
    int oldY = mScrollY;
    mScrollX = x;
    mScrollY = y; 
    onScrollChanged(mScrollX, mScrollY, oldX, oldY);
    if (!awakenScrollBars()) {
      invalidate();
    } 
  }  
} 

/** 
  * 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方法内部也是调用了scrollTo方法来实现其功能的。以上源码中我们注意到了mScrollX和mScrollY成员变量,前者是View的左边缘减去View的内容的左边缘,后者是View的上边缘减去View的内容的上边缘。示意图如下:



上图中,黑色边框代表View在屏幕上对应的矩形区域,蓝色边框代表View的内容。在上图中,我们调用scrollTo/scrollBy把View向右滚动了一定距离。实际上实现的是View的内容的滚动,而View的四个位置参数是保持不变的。想一下我们平常使用ListView时,滚动的就是ListView的内容,而ListView本身在屏幕上的位置是不变的。上图中,黑色左边缘(即View的左边缘)减去蓝色左边缘(即View的内容的左边缘)即可得到mScrollX。由此我们还可以知道,向右滚动时mScrollX负的,向左滚动时mScrollX是正的。同理我们可以知道,向下滚动时,mScrollY是负的,向上滚动时,mScrollY是正的。

经过以上的分析,我们了解到使用scrollTo/scrollBy方法实现View的滑动是很简单直接的,那么简单的背后有什么代价呢?代价就是滑动不是“弹性的”,弹性滑动指的是View的滑动应该是一个先加速再逐渐减速到停止的过程,这样看起来很平滑,不会很突兀。scrollTo/scrollBy方法实现的滑动看起来就会很突兀,这样的用户体验很不好。在解决这个问题之前,我们先来看看实现View滑动的其他方法。

使用动画来实现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>

然后我们就可以通过以下代码实现目标View(targetView)的滑动:

final Animation anim = AnimationUtils.loadAnimation(this, R.anim.anim);
targetView.startAnimation(anim);

使用补间动画实现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的复杂的滑动效果只有通过动画才能比较方便的实现。

通过改变布局参数来实现View的滑动

通过改变布局参数来实现View的滑动的思想很简单:比如向右移动一个View,只需要把它的marginLeft参数增大,向其它方向移动同理,只需改变相应的margin参数。还有一种比较拐弯抹角的方法是在要移动的View的旁边预先放一个View(初始宽高设为0)。然后比如我们要向右移动View,只需把预先放置的那个View的宽度增大,这样就把View“挤”到右边了。代码示例如下:

MarginLayoutParams params = (MarginLayoutParams) mButton.getLayoutParams();
params.leftMargin += 100;
// 请求重新对View进行measure、layout
mButton.requestLayout();

以上代码即实现了把mButton向右滑动100px。通过改变布局参数来实现的滑动效果也不是平滑的。

使用Scroller来实现弹性滑动

上面我们提到了使用scrollTo/scrollBy方法实现View的滑动效果不是平滑的,好消息是我们可以使用Scroller方法来辅助实现View的弹性滑动。使用Scroller实现弹性滑动的惯用代码如下:

Scroller scroller = new Scroller(mContext); 

private void smoothScrollTo(int dstX, int dstY) {
  int scrollX = getScrollX();
  int scrollY = getScrollY();
  int deltaX = dstX - scrollX;
  int deltaY = dstY - scrollY;
  scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); 
  invalidate(); 
}

@Override
public void computeScroll() {
  if (scroller.computeScrollOffset()) { 
    scrollTo(scroller.getCurrX(), scroller.getCurY());
    // 用于在非UI线程中更新用户界面
    postInvalidate();
  }
}

我们来看一下以上的代码。首先我们获取到View的mScrollX参数并存到scrollX变量中,获取mScrollY并保存到scrollY变量中。然后计算要滑动的位移量,接着调用了scroller.startScroll()方法,我们来看看startScroll()方法的源码:

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; 
  . . .
}

从以上的源码我们可以看到,startScroll方法中并没有进行实际的滚动操作,而是把startX、startY、dx、dy等参数都保存了下来。那么究竟怎么实现View的滑动的呢?
我们先回到Scroller惯用代码。可以看到smoothScrollTo()方法中调用了invalidate方法,这个方法会请求重绘View,这会导致View的draw()的方法被调用,而draw()方法内部会调用computeScroll()方法。因此我们重写了computeScroll()方法,在其内部调用了scrollTo()方法,并传入mScroller.getCurrX()和mScroller.getCurrY()方法作为参数,这两个方法会分别获取到mCurrX和mCurrY成员变量。这两个成员变量会在computeScrollOffset()方法中被赋值,我们一起来看下相关代码:

 public boolean computeScrollOffset() { 
   ... 
   int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); 
   if (timePassed < mDuration) { 
     switch (mMode) { 
       case SCROLL_MODE: 
         // 根据开始滑动以来已经过的时间计算一个插值
         // 比如如果是匀速运动的话,这个值就会是已经经过的时间占总滑动时间的百分比
         final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); 
         mCurrX = mStartX + Math.round(x * mDeltaX); 
         mCurrY = mStartY + Math.rounc(y * mDeltaY);
         break;
         ...
       }
    }
    return true;
}

mCurrX和mCurrY表示本次滑动的目标位置。computeScrollOffset()方法返回true表示滑动过程还未结束,返回false则表示滑动结束。

通过以上的分析,我们大概了解了Scroller实现弹性滑动的原理:
invaldate()方法会导致View的draw()方法被调用,而draw()方法会调用computeScroll()方法,因此重写了computeScroll()方法,而computeScrollOffset()方法会根据时间的流逝动态的计算出很小的一段时间应该滑动多少距离。也就是把一次滑动拆分成无数次小距离滑动从而实现“弹性滑动”。

参考资料

《Android开发艺术探索》


长按或扫描二维码关注我们,让您利用每天等地铁的时间就能学会怎样写出优质app。

推荐阅读更多精彩内容