Android中实现滑动的方式有很多种:
通过View本身提供的scrollTo/scrollBy方法来实现滑动。
通过Scroller类来实现平滑的过渡。
通过动画给View施加平移效果来实现滑动。
是通过改变View的LayoutParams使View重新布局从而实现滑动。
通过延时策略实现滑动的效果。
使用ViewDragHelper实现滑动效果。
Android为了实现View的滑动,View提供专门的方法来实现该功能,那就是scrollTo和scrollBy:
方法 | 描述 |
---|---|
scrollTo(x, y) | 表示相对右上角移动到某个位置 |
scrollBy(dx, dy ) | 表示移动的增量为dx、dy。 |
注意:
下面演示让一个View向右移动100像素,然后再向下移动50像素:
final TextView tvView = (TextView) findViewById(R.id.tv_view); tvView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ((View)tvView.getParent()).scrollTo(-100, -50); } });
这里相对左上角是(100, 50),由于scrollTo()和scrollBy()是相对左上角的,所以传入的应该是负数(-100, -50)。
Android中大部分的滑动都是瞬间完成的,如果想要实现平滑的过度滑动,则需要使用到Scroller。
Scroller是用来实现平滑过度的一个类,它常用的方法如下:
方法 | 描述 |
---|---|
getCurrX() | 获取mScroller当前水平滚动的位置 |
getCurrY() | 获取mScroller当前竖直滚动的位置 |
getFinalX() | 获取mScroller最终停止的水平位置 |
getFinalY() | 获取mScroller最终停止的竖直位置 |
setFinalX(int newX) | 设置mScroller最终停留的水平位置,直接跳到目标位置 |
setFinalY(int newY) | 设置mScroller最终停留的竖直位置,直接跳到目标位置 |
startScroll(int startX, int startY, int dx, int dy) | startX, startY为开始滚动的位置,dx,dy为滚动的偏移量 |
startScroll(int startX, int startY, int dx, int dy, int duration) | 同上,在上面的基础上多了duration完成滚动的时间 |
computeScrollOffset() | 判断是否滚动结束:true:未完成、false:已完成 |
1、Scroler的使用需要完成几部分操作,首先初始化Scroller类:
mScroller = new Scroller(context);
2、重写computeScroller()方法来实现模拟滑动:
@Override public void computeScroll() { super.computeScroll(); // 判断Scroller是否执行完毕 if(mScroller.computeScrollOffset()) { ((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 通过重绘来不断调用computeScroll()方法 invalidate(); } }
这是一个模板的写法,表示不断的通过重绘来调用该方法,以此判断是否滑动完成。
3、startScroll开启模拟过程
mScroller.startScroll(startX, startY, dx, dy, duration);
通过以上三步就可以使用Scroller来实现平滑移动了。
实例教程如下:
public class ScrollerView extends View { private Scroller mScroller; public ScrollerView(Context context, AttributeSet attrs) { super(context, attrs); mScroller = new Scroller(context); } // 对外提供移动的方法 public void smoothScrollTo(int dx, int dy, int duration) { mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration); invalidate(); } @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } }
scroller的源码如下所示:
Scroller mScroller = new Scroller(getContext()); // 缓慢滚动到指定位置 private void smoothScrollTo(int destX , int destY){ int scrollX = getScrollX(); int scrollY = getScrollY(); int delta = destX - scrollX; // 1000ms内滑向destX,缓慢移动 mScroller.startScroll(scrollX, 0, delta, 0, 1000); invalidate(); } @Override public void computeScroll() { super.computeScroll(); if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
当我们构造一个Scroller对象,并调用它的startScroll方法时,Scroller内部其实只是保存了传递的几个参数,并没有其他操作:
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; }
该方法的参数含义非常清晰,startX和startY表示滑动的起点,dx和dy表示滑动的距离,而duration表示的是滑动的时间。其实产生滑动效果的是invalidate方法,该方法会导致View重绘,在View的draw方法中又会调用computeScroll方法,computeScroll方法在View中是一个空实现,因此,
需要我们自己去实现。
当View重绘后会在draw方法中调用computeScroll,而computeScroll又会向Scroller获取当前scrollX和scrollY,然后通过scrollTo方法实现滑动。
接着又调用postInvalidate方法进行二次重绘,这个重绘的过程和第一次一样,还是会导致computeScroll方法被调用,然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo方法滑动到新的位置,如此反复,直到整个滑动过程结束。
下面我们来看下computeScrollOffset方法的实现:
/** * Call this when you want to know the new location. If it returns true, * the animation is not yet finished. */ 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.round(x * mDeltaY); break; .... } } return true; }
该方法根据时间流逝的百分比来算出scrollX和scrollY所改变的百分比来计算出当前的值。返回true表示滑动未结束,false则表示滑动结束。
使用动画来移动View,主要操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画。
示例1:将一个View从原始位置向右下角移动100个像素。有两种方式实现,传统动画和属性动画
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true" android:zAdjustment="normal"> <translate android:duration="100" android:fromXDelta="0" android:fromYDelta="0" android:interpolator="@android:anim/linear_interpolator" android:toXDelta="100" android:toYDelta="100"/> </set>
/**同样可以采用链式调用的方式*/ ObjectAnimator animator = ObjectAnimator.ofFloat(mView, "tanslationX", 0, 100); animator.setDuration(100); animator.start();
注意:如果使用到属性动画,为了兼容3.0以下版本,需要采用开源动画库nineoldandroids。
ValueAnimator滑动
动画本身是一种渐进的过程,因为它具备天然的弹性滑动效果。这里我们模仿Scroller来实现View的弹性滑动,利用动画的特性,我们可以采用如下方式实现:
final int startX = 0; final int detalX = 100; final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000); animator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = animator.getAnimatedFraction(); mButton.scrollTo(startX + (int)(detalX * fraction), 0); } }); animator.start();
动画本质上没有作用在任何对象上,只是在1000ms内完成整个动画过程,利用该特性,我们就可以在动画的每一帧到来时获取动画完成的比例。然后根据
这个比例计算出当前View所要滑动的距离。当然我们还可以在onAnimationUpdate方法中实现其他操作。
比如我们将一个button向右平移100px,只需要将LayoutParams里的marginLeft参数值增加100px即可:
MarginLayoutParams params = (MarginLayoutParams) mButton.getLayoutParams(); params.width += 100; params.leftMargin += 100; mButton.requestLayout(); 或者 mButton.setLayoutParams(params);
延时策略的核心思想是通过发送一系列的延时消息从而达到一种渐进式的效果,具体可以使用Handler或View的postDelay方法以及线程的sleep方法。
下面我们以Handler为例,在1000ms内将View内容像左移动100像素:
private static final int MSG_SCROLL_TO = 0; private static final int FRAME_COUNT = 30; private static final int DELAYED_TIME = 33; private int mCount = 0; private Handler mHandler = new Handler(){ public void handleMessage(Message msg) { switch (msg.what) { case MSG_SCROLL_TO: mCount++; if(mCount <= FRAME_COUNT){ float fraction = mCount / (float)FRAME_COUNT; int scrollX = (int) (fraction * 100); mButton.scrollTo(scrollX, 0); mHandler.sendEmptyMessageDelayed(MSG_SCROLL_TO, DELAYED_TIME); } break; } } };