说起View的滑动效果,实现的方法有多种,例如使用动画,或者通过改变View的布局参数,其实除了这两种外,在Android中View已经为我们提供了scrollTo()和scrollBy()方法来实现滑动效果,这两个方法也是我们接下来要重点讨论的...
但是呢,这两个方法实现的滑动效果都是瞬时完成的,并不能带来良好的体验哦,所以此时就需要我们的重量级嘉宾Scroller登场了,通过Scroller可以实现View在一定时间间隔内的弹性滑动,以带来更好的视觉体验,这么说可能有点抽象,举个例子,当我们使用ViewPager时,手指滑动一段距离松开后ViewPager会自动的滑动一段距离后停止,这个效果就是通过Scroller实现的,这样说应该能更好的理解Scroller的用途了。
本着从理论到实践的原则,我们来一步步的揭秘Scroller...
1. 你应该知道的scrollTo()和scrollBy()
首先看一下这两个方法的源码:
1 2 3 4 5 6 7 8 9 10 |
/** * 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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * 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; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } |
两个方法第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动。第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动,单位都是像素。
不同的是,scrollTo()方法让View相对于初始的位置滚动某段距离,scrollBy()方法则是让View相对于当前的位置滚动某段距离。同时可以发现scrollBy()是通过scrollTo()方法实现的。
上边我们说过,两个方法第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动。这是为什么呢???看下scrollTo()方法中的mScrollX = x;
一行,我们传入的x值被赋值给了mScrollX ,mScroller又是什么东东呢?其实View有一个getScrollX(),继续走进源码的世界:
1 2 3 |
public final int getScrollX() { return mScrollX; } |
可以看到通过getScrollX()方法可以返回mScrollX,也就是View在 水平方向移动的距离,其实关于mScrollX的值有个变化规律:mScrollX等于View左边缘和View内容左边缘的水平方向x坐标的差值,同样的道理mScrollY 等于View上边缘和View内容上边缘的垂直方向y坐标的差值。
通过scrollTo()或scrollBy()实现View的移动,只是View的内容的移动,View本身的位置并不会发生改变。此时的View可以理解为一个ViewGroup,而内容可以理解成ViewGroup中的子View,假设View的坐标原点为(0, 0),如果View内容从左向右移动,则View的内容左边缘x值将大于0,而View的左边缘x值始终为0,所以View左边缘和View内容左边缘的水平方向x坐标的差值小于0,也就是mScrollX的值小于0,这也就解释了为什么当scrollTo()或scrollBy()的x参数为负值,看起来View是向右移动的,这样也就可以理解x参数为正值的情况了,同理纵向上的y参数值也是一个道理。
说了这么多scroll相关的方法,也该聊聊我们的Scroller了...
2. Scroller弹性滑动原理
首先看一下Scroller的典型用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Scroller mScroller = new Scroller(mContext); private void smoothScroll(int destX, int destY) { int scrollX = getScrollX(); int deltaX = destX - scrollX; mScroller.startScroll(scrollX, 0, deltaX, 0, 500); invalidate(); } @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } |
目测startScroll()方法和View的滑动有关,继续看源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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代表需要滑动的距离,duration代表整个滑动需要的时间。骗子...这里并没有View滑动的相关逻辑。那么View如何通过Scroller实现弹性滑动呢?其实是通过startScroll()下面的invalidate();
方法,略抽象,可能我们更多的知道invalidate()方法能够使得View重绘,其实奥秘就在这里,通过使View重绘,会间接的执行computeScroll()方法,继续看源码:
1 2 |
public void computeScroll() { } |
原来是一个空方法,所以需要我们自行实现,so sad...
而在computeScroll()中,我们首先通过computeScrollOffset()方法判断滑动是否结束,这个方法如何工作呢?继续看源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
public boolean computeScrollOffset() { if (mFinished) { return false; } 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; case FLING_MODE: final float t = (float) timePassed / mDuration; final int index = (int) (NB_SAMPLES * t); float distanceCoef = 1.f; float velocityCoef = 0.f; if (index < NB_SAMPLES) { final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + 1]; velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); distanceCoef = d_inf + (t - t_inf) * velocityCoef; } mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); // Pin to mMinX <= mCurrX <= mMaxX mCurrX = Math.min(mCurrX, mMaxX); mCurrX = Math.max(mCurrX, mMinX); mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); // Pin to mMinY <= mCurrY <= mMaxY mCurrY = Math.min(mCurrY, mMaxY); mCurrY = Math.max(mCurrY, mMinY); if (mCurrX == mFinalX && mCurrY == mFinalY) { mFinished = true; } break; } } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; } |
看第一个if条件,如果滑动动画已经finish,则返回fasle,继续看第二个if条件,如果执行滑动动画已经花费的时间小于整个滑动动画需要的时间,则计算出mCurrX和mCurrY的值,也就是当前View内容左边缘的x、y坐标的值,同时返回true,所以,如果滑动的动画未结束则返回true否则返回false。当返回true时,则调用scrollTo(mScroller.getCurrX(), mScroller.getCurrY())
进行View的滑动,其中参数mScroller.getCurrX()、mScroller.getCurrY()得到的就是上边的mCurrX和mCurrY,scrollTo之后继续执行invalidate()方法,此时又会导致View重绘,又一次执行computeScroll()方法...,这样的反复执行,直到computeScrollOffset()方法返回flase,即完成View的滑动。
通过上边的分析可以看出,仅仅通过Scroller,并不能实现View的滑动效果,同时需要配合View的invalidate()、computeScroll()、scrollTo()方法才可以完成。
说了这么多的理论,也该实践一下了...
3. 自定义开关按钮
首先看下效果:
录制的效果不好,有兴趣可自行下载源码体验哦...
大概的实现思路是这样的, 通过自定义ViewGroup,并设置开关背景,其中滑块是ImageView,将滑块作为子View添加到ViewGroup中,然后监听ViewGroup的onTouchEvent处理相关手势,以及给滑块设置点击事件,来实现滑块的移动效果。当然这只是一个简单实现方式,可能存在缺陷,如有问题,欢迎拍砖...
具体的看下onTouchEvent方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); isValidToggle = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (!mScroller.isFinished()) { mScroller.abortAnimation(); } break; case MotionEvent.ACTION_MOVE: int deltaX = mLastX - x; //边界检测判断,防止滑块越界 if (deltaX + getScrollX() > 0) { scrollTo(0, 0); return true; } else if (deltaX + getScrollX() + getMeasuredWidth() / 2 < 0) { scrollTo(-getMeasuredWidth() / 2, 0); return true; } scrollBy(deltaX, 0); break; case MotionEvent.ACTION_UP: //处理弹性滑动 smoothScroll(); break; } mLastX = x; return super.onTouchEvent(event); } |
当手势为MotionEvent.ACTION_MOVE时,通过scrollBy(deltaX, 0);
实现滑块跟随手指的移动,但是滑块的滑动范围只能在ViewGroup中,所以我们进行了边界判断,如果发现越界,scrollTo()方法回到相应的边界。
当手势为MotionEvent.ACTION_UP时,即手指抬起,则开始进行我们的弹性滑动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private void smoothScroll() { int deltaX = 0; if (getScrollX() < -getMeasuredWidth() / 4) { deltaX = -getScrollX() - getMeasuredWidth() / 2; if (!isOpen) { isOpen = true; isValidToggle = true; } } if (getScrollX() >= -getMeasuredWidth() / 4) { deltaX = -getScrollX(); if (isOpen) { isOpen = false; isValidToggle = true; } } mScroller.startScroll(getScrollX(), 0, deltaX, 0, 500); invalidate(); } |
主要的逻辑就是判断手指抬起时滑块应该滚动到ViewGroup的左边还是右边,已经相应的deltaX值,然后开始滑动操作。
最后看下computeScroll()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } else { if (isValidToggle) { if (mToggleListener != null) { mToggleListener.onToggled(isOpen); Log.e("isOpen", isOpen + ""); } } } } |
其中的if结构在前边已经分析过了,在else中,先判断是否是一次有效的打开或关闭操作,如果是则通过接口回调来方便后续具体操作的扩展。
相信到这里,大家对Scroller以及如何实现View的滑动效果已经有所了解...