目前移動設(shè)備流行,我們要在如此小的屏幕上盡可能給用戶展現(xiàn)更多的內(nèi)容,就需要在應(yīng)用上通過滑動來顯示和隱藏部分內(nèi)容,View作為呈現(xiàn)內(nèi)容的媒介,具備滑動功能就無可厚非了。
三種方式實現(xiàn)View的滑動:
- 通過View本身提供的scrollTo/scrollBy方法
- 通過動畫給View添加平移動畫
- 通過改變View的LayoutParams使得View重新布局
通過使用scrollTo/scrollBy方法
先看這兩個方法的源碼實現(xiàn)
/**
* 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();
}
}
}
/**
* 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方法實際上也是調(diào)用了scrollTo方法,它實現(xiàn)了基于當(dāng)前位置的相對滑動,而scrollTo則是實現(xiàn)了基于所傳遞參數(shù)的絕對滑動。在滑動過程中,mScrollX的值總是等于View左邊緣和View內(nèi)容左邊緣在水平方向的距離,而mScrollY的值總是等于View上邊緣和View內(nèi)容上邊緣在豎直方向的距離,View的邊緣指的是View的位置,由四個頂點組成,View內(nèi)容邊緣指的是View中的內(nèi)容的邊緣,scrollTo和scrollBy只能改變View內(nèi)容的位置而不能改變View在布局中的位置。mScrollX和mScrollY的單位是像素。當(dāng)View左邊緣在View內(nèi)容左邊緣右邊時,mScrollX為正值,反之為負(fù)值;當(dāng)View上邊緣在View內(nèi)容上邊緣的下邊時,mScrollY為正值,反之為負(fù)值。
情景推理
比如我們有一個自定義View,占滿整個屏幕,那么這個View的的位置是固定了的,四個頂點分別是屏幕的四個頂點,這個是View怎么滑動都改變不了的,這個場景中View的邊緣就是屏幕的四條邊,不滑動時,這個View的邊緣和View的內(nèi)容邊緣是重合的。當(dāng)我下拉時,View向下移動,此時View的邊緣還是屏幕的四條邊,而View內(nèi)容的上邊緣變了。按照上面說的規(guī)則,此時View的上邊緣在View內(nèi)容上邊緣的上邊,即從上往下滑動時,mScrollY是負(fù)值,同理推得從左往右滑動時,mScrollX也是負(fù)值。
通過使用動畫實現(xiàn)
使用平移動畫主要是操作View的translationX和translationY這兩個屬性,那我們就有兩種選擇了,傳統(tǒng)的補(bǔ)間動畫和屬性動畫,如果使用屬性動畫,為了兼容Android3.0,需要采用開源動畫庫nineoldandroids。
在3.0以下系統(tǒng)的手機(jī)上通過nineoldandroids實現(xiàn)的屬性動畫本質(zhì)上還是補(bǔ)間動畫。
之前的兩篇關(guān)于動畫的文章中有實例平移動畫的,這里就不貼代碼了。
記得那時候有個問題不是很理解,就是當(dāng)使用補(bǔ)間動畫來對View做平移或者縮放時,加入View上有點擊事件,那么這個有效點擊區(qū)域還是原來View原始的位置區(qū)域,現(xiàn)在更加容易理解了,因為View的尺寸都沒改變,如果是使用屬性動畫,那么這個有效點擊區(qū)域就變了。
通過改變View的LayoutParams
這種方式其實就是改變View的外邊距,也就是margin值:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
params.leftMargin += 10;
params.width += 10;
v.setLayoutParams(params);
// 或者
//v.requestLayout();
幾種方式的對比
scrollTo/scrollBy:優(yōu)點是它是View提供的原生方法,專門用于View的滑動,可以比較方便地實現(xiàn)滑動效果且不影響內(nèi)部元素的點擊事件;缺點是只能滑動View的內(nèi)容,并不能滑動View本身。
動畫方式:如果是Android3.0以上且采用屬性動畫,則沒有明顯的缺點,但如果采用補(bǔ)間動畫或者在Android3.0以下的版本使用屬性動畫,則不能改變View本身的屬性。如果動畫元素不需要響應(yīng)用戶的交互,使用動畫來滑動比較合適,否則就不太合適。動畫有一個很明顯的優(yōu)勢就是一些復(fù)雜的滑動效果必須使用動畫才能實現(xiàn)。
改變布局方式:除了使用比較麻煩,沒有明顯的缺點,主要適用對象是一些具有交互性的View。
Demo:采用動畫的方式實現(xiàn)View的全屏滑動
思路就是重寫View的onTouchEvent方法,處理其中的ACTION_MOVE事件:
private int mLastX, mLastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
int translationX = (int) ViewHelper.getTranslationX(this) + deltaX;
int translationY = (int) ViewHelper.getTranslationY(this) + deltaY;
ViewHelper.setTranslationX(this, translationX);
ViewHelper.setTranslationY(this, translationY);
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return true;
}
首先我們通過event.getRawX()和event.getRawY()獲取手指當(dāng)前的坐標(biāo),不能使用getX/getY方法,因為是要全屏滑動,所以需要獲取當(dāng)前點擊事件在屏幕中的坐標(biāo)而不是相對于View本身的坐標(biāo)。然后計算出兩次滑動之間的距離,通過nineoldandroids庫提供的setTranslationX/setTranslationY方法來實現(xiàn)。
nineoldandroids的jar包下載地址:https://github.com/JakeWharton/NineOldAndroids/downloads
彈性滑動
實現(xiàn)彈性滑動的思路就是將一次大的滑動分成若干次小的滑動在一段時間內(nèi)完成,實現(xiàn)方式有Scroller、Handler#postDelayed、Thread#sleep等。
使用Scroller
其實上面已經(jīng)貼過使用Scroller實現(xiàn)滑動的核心代碼:
mScroller = new Scroller(context);
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
當(dāng)我們構(gòu)建了一個Scroller并調(diào)用其startScroll方法時,Scroller內(nèi)部其實什么都沒做,它只是保存了我們傳進(jìn)去的幾個參數(shù):
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
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表示滑動的時間,需要注意的是這里的滑動是指View內(nèi)容的滑動而不是View本身位置的改變。所以僅僅調(diào)用startScroll時無法讓View滑動的,因為它內(nèi)部沒有做滑動相關(guān)的事。使View滑動的真正起作用的代碼是invalidate方法,調(diào)用invalidate表示要重繪View,在View的draw方法中又會去調(diào)用computeScroll方法,computeScroll在View中是一個空實現(xiàn),因此需要我們自己去動手實現(xiàn)。正是因為這個computeScroll方法,View才能實現(xiàn)彈性滑動。過程是這樣的:當(dāng)View重繪后再draw方法中調(diào)用computeScroll方法,而computeScroll又會去向Scroller獲取當(dāng)前的scrollX和scrollY,然后通過scrollTo實現(xiàn)滑動,接著又調(diào)用postInvalidate方法再進(jìn)行一次重繪,和上次一樣,使View滑動到新的位置,一直重復(fù)這個過程,知道滑動結(jié)束。
下面是Scroller的computeScrollOffset方法:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
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;
...
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
這個方法會根據(jù)時間的流逝計算出當(dāng)前的scrollX和scrollY,計算方法是根據(jù)時間流逝的百分比計算出scrollX和scrollY改變的百分比并計算出當(dāng)前的值,是不是感覺和動畫中的插值器類似?沒錯,這里正是使用了叫ViscousFluidInterpolator插值器:代碼如下:
static class ViscousFluidInterpolator implements Interpolator {
/** Controls the viscous fluid effect (how much of it). */
private static final float VISCOUS_FLUID_SCALE = 8.0f;
private static final float VISCOUS_FLUID_NORMALIZE;
private static final float VISCOUS_FLUID_OFFSET;
static {
// must be set to 1.0 (used in viscousFluid())
VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
// account for very small floating-point error
VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
}
private static float viscousFluid(float x) {
x *= VISCOUS_FLUID_SCALE;
if (x < 1.0f) {
x -= (1.0f - (float)Math.exp(-x));
} else {
float start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float)Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
return x;
}
@Override
public float getInterpolation(float input) {
final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
if (interpolated > 0) {
return interpolated + VISCOUS_FLUID_OFFSET;
}
return interpolated;
}
}
這個插值器的實現(xiàn)過程我們先不管,繼續(xù)看computeScrollOffset方法,當(dāng)它的返回值是true時表示滑動還未結(jié)束,此時還要繼續(xù)滑動,當(dāng)返回false時表示滑動結(jié)束。
總結(jié):Scroller的工作原理
Scroller本身并不能實現(xiàn)View的滑動,而是要結(jié)合View的computeScroll方法才能完成彈性滑動的效果,它不斷讓View去重繪,而每一次重繪距滑動起始時間會有一個時間間隔,通過這個時間間隔就可以計算出View當(dāng)前的滑動位置,知道了滑動位置就可以通過scrollTo方法來完成View的滑動。
延時策略
延時策略實現(xiàn)彈性滑動的核心思想就是通過發(fā)送一系列延時消息來達(dá)到彈性的效果,具體可以使用Handler.postDelayed方法或者Thread.sleep方法。對于postDelayed方法,不斷的發(fā)送延時消息,就可以在消息中進(jìn)行View的移動,從而形成彈性滑動,而對于sleep方法,使用while循環(huán)不斷的滑動View然后sleep,同樣可以達(dá)到彈性的效果。
使用動畫
動畫本身就有一個duration屬性,相當(dāng)于它滑動時自帶了彈性效果,這部分在上面已經(jīng)說過了,就不再贅述了。