android自定義佈局中的平滑移動

android自定義佈局中的平滑移動。在android應用程序的開發過程中,相信我們很多人都想把應用的交互做的比較絢麗,比如讓界面切換平滑的滾動,還有熱度灰常高的偽3D等界面效果,通常情況下,系統提供的應用在特效這方面隻能為我們提供簡單的動畫接口,所以要想實現比較酷炫的效果還是要自己去開發佈局控件(即所謂的自定義View、ViewGroup)。

下面就我最近工作中遇到的一個自定義控件開發做一些簡單的介紹,其實那個地方原本可以用ScrollView解決很大一部分問題的,但有一些效果確實需要對控件進行重新定義,在繼承ScrollView開發中仍然會遇到一些ScrollView自身的限制,所以就仿照ScrollView自己做瞭一個控件。在其中遇到瞭一些問題自然就是像ScrollView中拖動的效果(比如快速拖動在手指離開屏幕時控件依舊會由於慣性繼續滑動一段距離後才會停止運動),所以就對這個東東做瞭一下仔細的研究,雖然以前也做過類似的開發,這次由於時間比較充裕,所以將開發中遇到的一些問題都一一記錄瞭下來。下面開始正題:

自定義佈局控件自然是要繼承某個View或ViewGroup

由於是根據項目的開發來寫的這篇博客,所以我就以自定義佈局控件(ViewGroup)來做介紹瞭。

開發一個自定義的ViewGroup自然是要繼承ViewGroup類瞭,在繼承這個類之後必須要重寫的方法就是

onLayout(boolean changed, int l, int t, int r, int b)

另外至少要有一個構造方法,我個人習慣重寫那個有兩個參數的構造方法(XXX(Context context, AttributeSet attrs)),因為有瞭這個構造方法就可以在xml佈局文件裡使用這個類瞭。

如果想要對這個佈局控件以及其子控件的尺寸進行精確的控制那就要重寫下面這個方法瞭

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

這個方法從字面理解就是估算控件的尺寸大小瞭。

下面開始介紹關於如何讓自定義的控件進行平滑的移動,並能夠根據手勢的情況產生慣性滑動的效果

先介紹一下開發這種滑動效果需要用到的各種工具類:

android.view.VelocityTracker

android.view.Scroller

android.view.ViewConfiguration

VelocityTracker從字面意思理解那就是速度追蹤器瞭,在滑動效果的開發中通常都是要使用該類計算出當前手勢的初始速度(不知道我這麼理解是否正確,對應的方法是velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity))並通過getXVelocity或getYVelocity方法得到對應的速度值initialVelocity,並將獲得的速度值傳遞給Scroller類的fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)方法進行控件滾動時各種位置坐標數值的計算,API中對fling方法的解釋是基於一個fling手勢開始滑動動作,滑動的距離將由所獲得的初始速度initialVelocity來決定。關於ViewConfiguration的使用主要使用瞭該類的下面三個方法:

configuration.getScaledTouchSlop()//獲得能夠進行手勢滑動的距離
configuration.getScaledMinimumFlingVelocity()//獲得允許執行一個fling手勢動作的最小速度值
configuration.getScaledMaximumFlingVelocity()//獲得允許執行一個fling手勢動作的最大速度值

需要重寫的方法至少要包含下面幾個方法:

onTouchEvent(MotionEvent event)//有手勢操作必然少不瞭這個方法瞭

computeScroll()//必要時由父控件調用請求或通知其一個子節點需要更新它的mScrollX和mScrollY的值。典型的例子就是在一個子節點正在使用Scroller進行滑動動畫時將會被執行。所以,從該方法的註釋來看,繼承這個方法的話一般都會有Scroller對象出現。

在往下就是介紹比較具體的開發思路

首先我們要初始化一些變量,其中的多數代碼已經在上面做出介紹瞭

Java代碼

  1. void init(Context context) {
  2. mScroller = new Scroller(getContext());
  3. setFocusable(true);
  4. setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
  5. setWillNotDraw(false);
  6. final ViewConfiguration configuration = ViewConfiguration.get(context);
  7. mTouchSlop = configuration.getScaledTouchSlop();
  8. mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
  9. mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
  10.  
  11. }

復制代碼

然後我們申明一個用來處理滑動操作的方法fling(int velocityY),代碼如下:

Java代碼

  1. public void fling(int velocityY) {
  2. if (getChildCount() > 0) {
  3. mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
  4. maxScrollEdge);
  5. final boolean movingDown = velocityY > 0;
  6. awakenScrollBars(mScroller.getDuration());
  7. invalidate();
  8. }
  9. }

復制代碼

在這個方法裡隻是使用Scroller的fling方法開始執行fling手勢動作瞭,關於其中的各種參數就不一一解釋瞭。

awakenScrollBars(int startDelay)方法根據我對註釋的理解就是在這裡給出動畫開始的延時,當參數startDelay為0時動畫將立刻開始,其實就是一個延遲的作用

下面是對VelocityTracker的初始化以及資源釋放的方法

Java代碼

  1. private void obtainVelocityTracker(MotionEvent event) {
  2. if (mVelocityTracker == null) {
  3. mVelocityTracker = VelocityTracker.obtain();
  4. }
  5. mVelocityTracker.addMovement(event);
  6. }
  7.  
  8. private void releaseVelocityTracker() {
  9. if (mVelocityTracker != null) {
  10. mVelocityTracker.recycle();
  11. mVelocityTracker = null;
  12. }
  13. }

復制代碼

onTouchEvent(MotionEvent event)方法的重寫

Java代碼

  1. public boolean onTouchEvent(MotionEvent event) {
  2. if (event.getAction() == MotionEvent.ACTION_DOWN
  3. && event.getEdgeFlags() != 0) {
  4. return false;
  5. }
  6.  
  7. obtainVelocityTracker(event);
  8.  
  9. final int action = event.getAction();
  10. final float x = event.getX();
  11. final float y = event.getY();
  12.  
  13. switch (action) {
  14. case MotionEvent.ACTION_DOWN:
  15. LogUtil.log(TAG, "ACTION_DOWN#currentScrollY:" + getScrollY()
  16. + ", mLastMotionY:" + mLastMotionY,
  17. LogUtil.LOG_E);
  18. if (!mScroller.isFinished()) {
  19. mScroller.abortAnimation();
  20. }
  21. mLastMotionY = y;
  22. break;
  23.  
  24. case MotionEvent.ACTION_MOVE:
  25. final int deltaY = (int) (mLastMotionY – y);
  26. mLastMotionY = y;
  27. if (deltaY < 0) {
  28. if (getScrollY() > 0) {
  29. scrollBy(0, deltaY);
  30. }
  31. } else if (deltaY > 0) {
  32. mIsInEdge = getScrollY() <= childTotalHeight – height;
  33. if (mIsInEdge) {
  34. scrollBy(0, deltaY);
  35. }
  36. }
  37. break;
  38.  
  39. case MotionEvent.ACTION_UP:
  40. final VelocityTracker velocityTracker = mVelocityTracker;
  41. velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
  42. int initialVelocity = (int) velocityTracker.getYVelocity();
  43.  
  44. if ((Math.abs(initialVelocity) > mMinimumVelocity)
  45. && getChildCount() > 0) {
  46. fling(-initialVelocity);
  47. }
  48.  
  49. releaseVelocityTracker();
  50. break;
  51. }
  52.  
  53. return true;
  54. }

復制代碼

在onTouchEvent方法中,當手勢執行到ACTION_UP時獲得當時手勢的速度值然後判斷這個速度值是否大於可滑動的最小速度,如果符合條件那麼就執行fling(int velocityY)方法,通過fling方法中的日志發現,在執行瞭invalidate()方法之後,程序便會執行computeScroll()方法,在computeScroll()方法中執行scrollTo方法主要是因為mScrollX、mScrollY這兩個變量的修飾符為portected,無法在擴展類裡面無法對這兩個變量直接進行操作,那麼就需要使用scrollTo方法對這兩個變量進行操作,以刷新當前的UI控件,下面附上computeScroll()方法的代碼

Java代碼

  1. public void computeScroll() {
  2. if (mScroller.computeScrollOffset()) {
  3. int scrollX = getScrollX();
  4. int scrollY = getScrollY();
  5. int oldX = scrollX;
  6. int oldY = scrollY;
  7. int x = mScroller.getCurrX();
  8. int y = mScroller.getCurrY();
  9. scrollX = x;
  10. scrollY = y;
  11. scrollY = scrollY + 10;
  12. scrollTo(scrollX, scrollY);
  13. postInvalidate();
  14. }
  15. }

復制代碼

其中的mScroller.computeScrollOffset()是用來判斷動畫是否完成,如果沒有完成返回true繼續執行界面刷新的操作,各種位置信息將被重新計算用以重新繪制最新狀態的界面。關於scrollTo方法,我們需要看一下該方法的代碼(來自View中):

Java代碼

  1. public void scrollTo(int x, int y) {
  2. if (mScrollX != x || mScrollY != y) {
  3. int oldX = mScrollX;
  4. int oldY = mScrollY;
  5. mScrollX = x;
  6. mScrollY = y;
  7. onScrollChanged(mScrollX, mScrollY, oldX, oldY);
  8. if (!awakenScrollBars()) {
  9. invalidate();
  10. }
  11. }
  12. }

復制代碼

我們可以看到,當傳遞進來的x、y的值與控件當前的mScrollX、mScrollY的值不相同時對界面進行重新計算,根據日志打印的情況來看似乎awakenScrollBars()返回的總是true, 這樣的話每執行一次computeScroll()方法,就需要執行一次postInvalidate()方法來刷新界面,而postInvalidate()方法會通過內部線程重新調用invalidate()已達到界面刷新的效果,產生手勢離開屏幕之後的慣性滑動效果。

可能上面說的比較凌亂,在這裡總結一下,大概的思路如下:

首先我們通過VelocityTracker、ViewConfiguration類得到一些慣性滑動所必須的變量,比如手勢離開屏幕時的初始速度,允許進行手勢操作的最小距離以及允許手勢操作的速度邊界值;

第二,創建Scroller的對象,使用它的fling方法供我們控制界面滑動使用;

第三,重寫onTouchEvent方法,當我們用手指在屏幕上來回滑動時此時執行的是scrollBy方法來刷新界面,當手指離開屏幕,此時就要開始執行ACTION_UP後面的操作瞭;

通過對手指離開屏幕時的速度進行判斷是否能夠進行慣性滑動操作,

如果能夠執行那麼就使用Scroller類的fling方法啟動滑動動畫,

這時需要調用一下invalidate()方法來間接的調用computeScroll方法,

在computeScroll方法中對Scroller的動畫是否執行完成做瞭判斷,

如果動畫沒有完成(mScroller.computeScrollOffset() == true)那麼就使用scrollTo方法對mScrollX、mScrollY的值進行重新計算刷新界面,

調用postInvalidate()方法重新繪制界面,

postInvalidate()方法會調用invalidate()方法,

invalidate()方法又會調用computeScroll方法,

就這樣周而復始的相互調用,直到mScroller.computeScrollOffset()返回false才會停止界面的重繪動作

總結,滑動效果來看,它依然是在不停的計算控件的位置刷新屏幕,不停的繪制新的圖片替換舊的圖片,當然每次刷新的速度很快,從而給人一種是在快速滑動的感覺,寫到這裡我發現,現在所謂的動畫總是逃脫不瞭電影的那種模式,每秒播放多少幀的圖片來達到連續播放的效果欺騙人的眼睛。

而且,關於android一些酷炫效果的開發,還是要自己多動手,熟悉View、ViewGroup中每個繪制方法、位置計算方法的調用方式以及順序,那麼至少是在2D動畫開發中,也就是一種方式,逃脫不瞭不停重新繪制的這個圈。

關於熟悉View、ViewGroup中每個繪制方法、位置計算方法的調用方式以及順序的問題,我建議最好自己寫一個簡單的自定義View或ViewGroup的擴展類,重載那些繪制、位置計算的方法打個日志出來一看自然就明白瞭,雖然這個方法很笨,但是很容易出效果的。

You May Also Like