Android應用開發Scroller詳解及源碼淺析
1 背景
大家都知道Android View提供了scrollTo()與scrollBy()方法來供我們進行View的滾動,但是有個問題就是他的滾動很蛋疼,疼在是瞬時挪動到指定位置的,這種對於追求使用者體驗的今天來說簡直是硬傷啊;為瞭解決這個問題Google給我們提供了一個牛叉的工具類Scroller,下面我們就深入淺出的來開戰這一工具類,將其玩爆,以便日後自訂控制項時如魚得水。
Scroller可以讓我們的滾動變得十分優雅,可以瞬間提升我們自訂控制項的逼格,但是瞭解該篇之前請先吃飽《Android應用座標系統全面詳解》一文,因為他們關係十分密切;當然喏,當你看完本文如果想看看Google自己對Scroller高端的使用則還可以繼續看看《Android應用ViewDragHelper詳解及部分源碼淺析》一文,哈哈。
PS:要過年了,公司一片動蕩。。。。。。
2 Scroller基礎執行個體
和以前博文一樣,開始源碼分析前先給出一個使用的基本例子作為引導,否則都不知道自己在看啥。這裡我們給出一個比較常見的東東—–側滑拉出收合(類似QQ List列表Item的效果)。如下是控制項Demo效果(請原諒我Ubuntu gif):
樣本源碼點我下載,不過只是Demo給出思路,細節沒有處理,也沒有進行完善,只是作為Scroller的Demo。下面是該控制項實現的核心代碼:
public class HorizontalFlingLayout extends LinearLayout { private Scroller mScroller; private View mLeftView; private View mRightView; private float mInitX, mInitY; private float mOffsetX, mOffsetY; public HorizontalFlingLayout(Context context) { this(context, null); } public HorizontalFlingLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public HorizontalFlingLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { this.setOrientation(LinearLayout.HORIZONTAL); mScroller = new Scroller(getContext(), null, true); } @Override protected void onFinishInflate() { super.onFinishInflate(); if (getChildCount() != 2) { throw new RuntimeException("Only need two child view! Please check you xml file!"); } mLeftView = getChildAt(0); mRightView = getChildAt(1); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: mInitX = ev.getX(); mInitY = ev.getY(); super.dispatchTouchEvent(ev); return true; case MotionEvent.ACTION_MOVE: //>0為手勢向右下 mOffsetX = ev.getX() - mInitX; mOffsetY = ev.getY() - mInitY; //橫向手勢跟隨移動 if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > ViewConfiguration.getTouchSlop()) { int offset = (int) -mOffsetX; if (getScrollX() + offset > mRightView.getWidth() || getScrollX() + offset < 0) { return true; } this.scrollBy(offset, 0); mInitX = ev.getX(); mInitY = ev.getY(); return true; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: //鬆手時刻滑動 int offset = ((getScrollX() / (float)mRightView.getWidth()) > 0.5) ? mRightView.getWidth() : 0;// this.scrollTo(offset, 0); mScroller.startScroll(this.getScrollX(), this.getScrollY(), offset-this.getScrollX(), 0); invalidate(); mInitX = 0; mInitY = 0; mOffsetX = 0; mOffsetY = 0; break; } return super.dispatchTouchEvent(ev); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }}
簡單吧,使用Scroller就能這麼優雅的滑動,不解釋,簡單的Demo,哈哈;有了這個基本映像我們直接高速——源碼探測,搞清源碼基本原理流程就能用的順手嘍。
3 Scroller源碼淺析
通過上面執行個體我們可以發現在自訂View的過程中使用Scroller的流程如所示:
既然有了這麼明確的流程圖,那我們下面就來依據這個流程簡單分析下Scroller的源碼。可以發現Scroller這類的代碼不多哇,確實是一個工具類,哈哈,我們先看下構造方法:
public Scroller(Context context) { this(context, null);}public Scroller(Context context, Interpolator interpolator) { this(context, interpolator, context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);}public Scroller(Context context, Interpolator interpolator, boolean flywheel) { mFinished = true; if (interpolator == null) { mInterpolator = new ViscousFluidInterpolator(); } else { mInterpolator = interpolator; } mPpi = context.getResources().getDisplayMetrics().density * 160.0f; //摩擦力計算單位時間減速度 mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); mFlywheel = flywheel; mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning}
可以看見,構造方法沒啥特殊的,只是一些基礎的設定,唯一要重點關注可能自訂的也就動畫插值器那個參數了,預設是ViscousFluidInterpolator的,我們可以自訂修改。兩參構造方法中其實也就是對第三個參數做了HONEYCOMB相容性處理,三參是所有構造方法最終調運的方法,其實也就是初始化了一些變數而已,沒啥重要的。
下面我們看看與Scroller相關的startScroll()和fling()方法,源碼如下:
//在我們想要滾動的地方調運,準備開始滾動,預設滾動時間為DEFAULT_DURATIONpublic void startScroll(int startX, int startY, int dx, int dy) { startScroll(startX, startY, dx, dy, DEFAULT_DURATION);}//在我們想要滾動的地方調運,準備開始滾動,手動設定滾動時間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;}//在快速滑動鬆開的基礎上開始慣性滾動,滾動距離取決於fling的初速度public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { ...... mMode = FLING_MODE; mFinished = false; ...... mStartX = startX; mStartY = startY; ...... mDistance = (int) (totalDistance * Math.signum(velocity)); mMinX = minX; mMaxX = maxX; mMinY = minY; mMaxY = maxY; ...... mFinalY = Math.min(mFinalY, mMaxY); mFinalY = Math.max(mFinalY, mMinY);}
可以看見,上面這幾個美其名曰滑動的Scroller方法其實都只是一個幌子,沒有進行滑動,而是初始化了一堆成員變數;譬如滾動模式、開始時間、期間等,也就是說他們都只是工具方法而已,實質的滑動其實是需要我們在他後面手動調運View的invalidate()進行重新整理,然後在View進行重新整理時又會調運自己的View.computeScroll()方法(不瞭解View繪製的請看《Android應用程式層View繪製流程與源碼分析》一文),在View.computeScroll()方法中進行Scroller.computeScrollOffset()判斷與觸發View的滑動方法。
既然這樣那我們粗略給出View的繪製流程,詳細的請看《Android應用程式層View繪製流程與源碼分析》一文。當我們調運invalidate()會觸發View的如下方法:
public void draw(Canvas canvas) { ...... /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ ...... // Step 4, draw the children dispatchDraw(canvas); ......}
可以發現,View的draw()方法被觸發時總共會進行6步,最重要的一步我們看第四步,下面是第四步dispatchDraw()方法源碼:
protected void dispatchDraw(Canvas canvas) {}
可以看見,View的該方法為空白方法,那我們看下他子類ViewGroup的該方法,如下:
protected void dispatchDraw(Canvas canvas) { ...... for (int i = 0; i < childrenCount; i++) { ...... more |= drawChild(canvas, child, drawingTime); ...... } ......}
可以發現,ViewGroup的dispatchDraw()方法實質又跑到了drawChild()方法,如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime);}
額額,實質又是child的另一個draw()方法而已,我們回到View去看下這個方法,如下:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { ...... if (!drawingWithRenderNode) { computeScroll(); sx = mScrollX; sy = mScrollY; } ......}
額額,這就解釋了為何View調運invalidate()就會觸發computeScroll()方法了。而ViewGroup最終調運scrollTo()方法都只能滾動內部子View的問題其實是因為ViewGroup它本身並沒有任何可畫的東西,它是一個透明的控制項,所以一般不會觸發onDraw()方法,但是當你給他設定背景等就會調用onDraw方法了,可是走的是繪製背景流程。
View相關的扯完了,下面我們來看看Scroller的computeScrollOffset()方法,下面我們簡單分析這個方法,如下:
//判斷滾動是否還在繼續,true繼續,false結束public boolean computeScrollOffset() { //mFinished為true表示已經完成了滑動,直接返回為false if (mFinished) { return false; } //mStartTime為開始時的時間戳記,timePassed就是當前滑動期間 int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); //mDuration為我們設定的期間,噹噹前已滑動耗時timePassed小於總設定期間時才進入if if (timePassed < mDuration) { //mMode有兩中,如果調運startScroll()則為SCROLL_MODE模式,調運fling()則為FLING_MODE模式 switch (mMode) { case SCROLL_MODE: //根據Interpolator插值器計算在該時間段裡移動的距離賦值給mCurrX和mCurrY final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; case FLING_MODE: //各種數學運算擷取mCurrY、mCurrX,實質類似上面SCROLL_MODE,只是這裡時慣性的 ...... // 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 { //認為滑動結束,mFinished置位true,標記結束,下一次再觸發該方法時一進來就判斷返回false了 mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true;}
可以看見該方法的作用其實就是Realtime Compute滾動的位移量(也是一個工具方法),同時判斷滾動是否結束(true代表沒結束,false代表結束)。
到此整個Scroller就分析完了,剩下的全是各種getXXX、setXXX方法就沒啥意思了。
4 Scroller總結
基於上面的例子和分析我們進行如下總結:
public class Scroller { ...... public Scroller(Context context) {} public Scroller(Context context, Interpolator interpolator) {} public Scroller(Context context, Interpolator interpolator, boolean flywheel) {} //設定滾動期間 public final void setFriction(float friction) {} //返復原動是否結束 public final boolean isFinished() {} //強制終止滾動 public final void forceFinished(boolean finished) {} //返復原動期間 public final int getDuration() {} //返回當前滾動的位移量 public final int getCurrX() {} public final int getCurrY() {} //返回當前的速度 public float getCurrVelocity() {} //返復原動起始點位移量 public final int getStartX() {} public final int getStartY() {} //返復原動結束位移量 public final int getFinalX() {} public final int getFinalY() {} //即時調用該方法擷取座標及判斷滑動是否結束,返回true動畫沒結束 public boolean computeScrollOffset() {} //滑動到指定位置 public void startScroll(int startX, int startY, int dx, int dy) {} public void startScroll(int startX, int startY, int dx, int dy, int duration) {} //快速滑動鬆開手勢慣性滑動 public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {} //終止動畫,滾到最終的x、y位置 public void abortAnimation() {} //延長滾動的時間 public void extendDuration(int extend) {} //返復原動開始經過的時間 public int timePassed() {} //設定終止時位移量 public void setFinalX(int newX) {} public void setFinalY(int newY) {}}
至此Scroller就結束了,相關問題可以自行腦補,相信有了該篇的協助你的自訂之路又暫時明朗了一段。