android 動畫原理二

鴨脖發表於2012-07-16

簡介: 

這是由兩部分組成的 Android 動畫框架詳解的第二部分例項篇。在閱讀本篇之前,建議您首先閱讀本系列的第一部分 Android 動畫框架詳解之原理篇。原理篇詳細介紹了 Android 動畫框架的實現原理,同時介紹了一個繞 Y 軸旋轉的動畫示例。本篇是在原理篇的基礎上介紹一個較複雜的 Android launcher 的平滑和立體翻頁效果動畫的實現。

 

 

Android launcher 的平滑和立體翻頁效果

我們這裡把 Android launcher 程式的 Workspace 相關的程式碼抽取出來,以一個比較簡單的程式碼來展示 launcher 程式是如何實現多頁以及不同頁面之間的切換效果。本示例程式碼在 SDK 2.1 中執行,設定的是 WVGA 的螢幕大小。

首先我們來看一下程式執行的效果來一些感性的認識。


圖 1:平滑移動效果
圖 1:平滑移動效果 

圖 2:立體翻頁效果
圖 2:立體翻頁效果 

視窗頁面的佈局

接著我們來看一下程式 UI(即 View 和 ViewGroup)的佈局,Activity 的 ContentView 是 layout 中的 main.xml。它的內容如下:


清單 1.

  1. <?xml version="1.0" encoding="utf-8"?>  
  2.   
  3. <LinearLayout xmlns:android=http://schemas.android.com/apk/res/android  
  4.   
  5.     android:orientation="vertical"  
  6.   
  7.     android:layout_width="fill_parent"  
  8.   
  9.     android:layout_height="fill_parent">  
  10.   
  11.     <com.easyandroid.workspace.FlatWorkspace android:id="@+id/workspace"  
  12.   
  13.         android:layout_width="fill_parent"  
  14.   
  15.         android:layout_height="fill_parent">  
  16.   
  17.    </com.easyanrodid.workspace.FlatWorkspace>  
  18.   
  19. </LinearLayout>  


 

其中 FlatWorkspace 的基類是 Workspace,它繼承自 ViewGroup,是一個容器類,其中包含三個子 View,子 View 是 ImageView。三個 ImageView 就是三個頁面。這三個 ImageView 的建立是在 WorkspaceActivity 的 onCreate 函式中呼叫 Workspace 的 initScreens 函式完成的,程式碼如下:

清單 2

  1. ViewGroup.LayoutParams p = new iewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,  
  2.                            ViewGroup.LayoutParams.FILL_PARENT);    
  3.   
  4. for (int i = 0; i < 3; i++)   
  5. {   
  6.     this.addView(new ImageView(this.getContext()), i, p);   
  7. }    
  8.   
  9. ((ImageView)this.getChildAt(0)).setImageResource(R.drawable.image_search);   
  10. ((ImageView)this.getChildAt(1)).setImageResource(R.drawable.image_system);   
  11. ((ImageView)this.getChildAt(2)).setImageResource(R.drawable.image_top);  


 

圖 3:Workspace 和頁面佈局圖
圖 3:Workspace 和頁面佈局圖 

為了讓三個頁面達到上圖的視窗布局,我們對 Workspace 的 onMeasure 和 onLayout 函式進行了過載,重點在 onLayout 程式碼中。onLayout 函式呼叫 layoutScreens 函式完成佈局,FlatWorkspace 中的 layoutScreens 實現如下:

清單 3

  1. protected void layoutScreens()   
  2. {   
  3.     int childLeft = 0;   
  4.     final int count = getChildCount();  
  5.    
  6.     for (int i = 0; i < count; i++)   
  7.     {   
  8.         final View child = getChildAt(i);   
  9.        
  10.         if (child.getVisibility() != View.GONE)  
  11.         {   
  12.             final int childWidth = child.getMeasuredWidth();   
  13.             child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());   
  14.             childLeft += childWidth;   
  15.         }   
  16.     }   
  17. }   


 

上面 child.layout 部分的程式碼把三個頁面分別佈局到了 X 和 Y 座標系中的((0,0)-(ScreenWidth,ScreenHeight))和((ScreenWidth,0)-(2*ScreenWidth,ScreenHeight))以及((2*ScreenWidth,0)-(3*ScreenWidth,ScreenHeight))三個矩形區域中,這裡用矩形區域的左上角頂點座標和右下角的頂點座標來表示矩陣。

至此我們已經完成了整個視窗頁面的佈局,視窗頁面的佈局大小是實際可視螢幕寬度的三倍,所以要顯示所有頁面需要讓頁面滾動。

頁面的平滑移動的實現

下面來看使用者 touch move 的時候程式如何讓頁面進行滑動,並且繪製他們。

頁面的滑動可以呼叫 View 的 scrollBy 或 ScrollTo 函式,在 Workspace 的 onTouchEvent 函式中取得使用者的手指移動的距離,然後呼叫 scrollBy(它的引數就是 X 和 Y 軸上需要移動的距離)來讓 Workspace 這個 View(也是 ViewGroup)移動使用者手指移動的距離,當然 View 移動之前得判斷一下使用者手指移動的距離和速度是否足夠才進行移動,以此減少使用者的誤操作。這部分程式碼簡單就不進行深入分析了,請大家自己看看程式碼。

當 Workspace 這個 View 呼叫 scrollBy 進行 View 的滾動時,必然導致這個 View 無效,從而被系統重新繪製,所以它的 dispatchDraw 函式會被呼叫來進行子 View(ImageView)的繪製,它本身沒有什麼東西要繪製,所以就不用關心 Workspace 的 onDraw 函式了。dispatchDraw 函式會呼叫 drawScreens(canvas) 來對子 View 進行繪製。我們來看一下 FlatWorkspace 的實現:

清單 4

  1. protected void drawScreens(Canvas canvas)   
  2. {   
  3.         final long drawingTime = getDrawingTime();   
  4.         final int count = getChildCount();   
  5.           
  6.         for (int i = 0; i < count; i++)   
  7.         {   
  8.             drawChild(canvas, getChildAt(i), drawingTime);   
  9.         }   
  10. }   


 

這裡的 canvas 寬高就是螢幕可視範圍的大小(如 HVGA 螢幕的 320 × 480 大小),而三個子 ImageView 的佈局要超出螢幕的範圍,不在螢幕可視範圍之內的部分是不會被繪製的。這個繪製三個子 ImageView 的函式很重要,是製作立方體翻頁等特效的關鍵地方,FlatWorkspace 實現的是平滑滑動效果,所以我們直接繪製三個子 ImageView。如果要實現立方體的效果,在繪製三個子 ImageView 的時候就要讓它們被繪製的時候有立體感,這個在 android 中我們可以通過上文提到的 Camera 類沿 Y 軸旋轉一定的角度實現。

程式讓使用者進行 touch move 操作的目的是讓使用者選擇一個頁面,如果按照上面的實現,當使用者最後抬起手指時,頁面切換不會很徹底,而是象圖 1 一樣停留在兩個頁面之間。所以當使用者抬起手指時程式需判斷一下移動到下一個完整的頁面還有多大距離,然後讓 Workspace 這個 View 再移動這個距離一遍完整的切換到下一頁。在這個移動的過程中,為了給使用者一個平滑的感覺,不能一下就移動這個距離,而是需要給一定的時間間隔,在這個時間段裡逐漸的移動到位,所以這裡我們使用 Scroller 類的方法實現逐漸的移動。具體過程是在 Workspace 的 onTouchEvent 函式中檢測到使用者 touch up(抬起手指)時進行應該調整到哪個頁面的判斷,然後呼叫 snapToScreen(targetScreen) 跳轉到需要目的頁面,然後它呼叫 scrollToScreen(screen) 讓 Workspace 這個 View 進行需要的滾動,這個函式在 FlatWorkspace 中的實現如下:

 

清單 5

  1. public void scrollToScreen(int screen)   
  2. {   
  3.     final int newX = screen * getWidth();   
  4.     final int deltaX = newX - getScrollX();   
  5.     Log.e("FlatWorkspace","scrollToScreen call mScroller.startScroll");   
  6.     mScroller.startScroll(getScrollX(), getScrollY(), deltaX, getScrollY(), Math.abs(deltaX) * 2);   
  7.     invalidate();   
  8. }   

這裡的重點是 mScroler.startScroll 部分的程式碼,它讓 Workspace view 在時間段 Math.abs(deltaX) * 2 裡移動下一個目標頁面視覺化需要移動的距離 deltaX(及目的頁面的座標減去目前已經移動的距離),大家請好好看一下這個 deltaX 的計算,這裡不細說了。這個 mScroller.startScroll 並不會導致 Workspace 立即進行移動,它只會導致當前 View 無效,從而重新繪製,在 Workspace 被它的父親 View 呼叫繪製的時候,它的 computeScroll 函式會被呼叫,所以會在這個函式中讓 Workspace 呼叫 scrollTo 函式進行實際的移動。程式碼如下:

 

清單 6

  1. public void computeScroll()   
  2. {   
  3.     if (mScroller.computeScrollOffset())   
  4.     {    
  5.         scrollTo(mScroller.getCurrX(), mScroller.getCurrY());   
  6.       //postInvalidate();   
  7.     }   
  8.     else if (mNextScreen != INVALID_SCREEN)   
  9.     {   
  10.         mCurrentScreen = mNextScreen;   
  11.         mNextScreen = INVALID_SCREEN;       
  12.     }   
  13. }   


 

至此,我們對 Workspace 的整個執行機制和平滑移動的效果是如何實現的已經介紹完成了。下面我們來具體談談立體翻頁效果是如何實現的。

立體翻頁效果的實現

通過前面的分析可知,立體翻頁效果可以在平滑翻頁效果的基礎上通過改寫三個子 ImageView 的繪製來完成。同時可知,翻頁時使用者操作過程分為三步:放下手指觸控螢幕,移動手指,抬起手指。手指觸控螢幕表示頁面之間的滑動要開始了;移動手指的時候頁面應該跟著使用者手指的移動距離進行對應距離的移動,同時系統會根據頁面的移動位置對 Workspace 裡面的三個子 View(即頁面)進行繪製;抬起手指的時候判斷應該移動到哪個頁面,還需要移動多少距離,然後平滑的移動需要的距離來跳轉到目的頁面上。

為了顯示立體效果,對每個子 ImageView 的繪製時得想辦法讓它沿 Y 軸旋轉一定的角度,前面已經提到 android 通過 Camera 這個類提供了這個功能,不需要使用 opengl ES 的東西,當然如果要做出更好的 3D 效果,我們就需要 opengl ES 的強大功能了。既然要旋轉一定的角度,那這個角度怎麼計算呢?我們把這個角度和使用者手指移動的距離關聯起來。因為這個立方體只會沿著 Y 軸旋轉,我們只看這三個面的立方體的頂部就夠了,它的頂部沿著 Y 軸的往其箭頭指示的方向看是一個等邊三角形,每個面相對於手機螢幕的沿著 Y 軸旋轉的角度的計算方法如下圖所示:


圖 4:初始螢幕位置示意圖
圖 4: 初始螢幕位置示意圖 

下圖為螢幕 1 沿 Y 軸旋轉 45 讀後其他兩個螢幕需要沿 Y 軸旋轉的角度。


圖 5:旋轉 45 度後螢幕位置示意圖
圖 5: 旋轉 45 度後螢幕位置示意圖

這個變換的部分請看程式碼 CubeWorkspace 中函式 drawScreen 的程式碼,如下:

清單 7

  1. protected void drawScreen(Canvas canvas, int screen, long drawingTime)   
  2. {   
  3.     final int width = getWidth();   
  4.     final int scrollWidth = screen * width;   
  5.     final int scrollX = this.getScrollX();    
  6.       
  7.     if(scrollWidth > scrollX + width || scrollWidth + width < scrollX)   
  8.     {   
  9.         return;   
  10.     }   
  11.       
  12.     final View child = getChildAt(screen);   
  13.     final int faceIndex = screen;   
  14.     final float faceDegree = currentDegree - faceIndex * preFaceDegree;   
  15.       
  16.     if(faceDegree > 90  faceDegree < -90)   
  17.     {   
  18.         return;   
  19.     }   
  20.       
  21.     final float centerX = (scrollWidth < scrollX)?scrollWidth + width:scrollWidth;   
  22.     final float centerY = getHeight()/2;   
  23.     final Camera camera = mCamera;   
  24.     final Matrix matrix = mMatrix;   
  25.     canvas.save();   
  26.     camera.save();   
  27.     camera.rotateY(-faceDegree);   
  28.     camera.getMatrix(matrix);   
  29.     camera.restore();   
  30.     matrix.preTranslate(-centerX, -centerY);   
  31.     matrix.postTranslate(centerX, centerY);   
  32.     canvas.concat(matrix);   
  33.     drawChild(canvas, child, drawingTime);   
  34.     child.setBackgroundColor(Color.TRANSPARENT);   
  35.     canvas.restore();  
  36.   
  37. }  


 

上面函式中的 currentDegree 變數是變化的,不是一個固定的值,改變這個變數值的方法比較隱蔽,在 AngelBaseWorkspace 的 scrollTo 函式中。AngelBaseWorkspace 中的 scrollTo 函式把 View 類中的函式過載了,這個函式會被 View 中的 scrollBy 函式呼叫,所以每次 touch 螢幕並且 move 的時候 AngelBaseWorkspace 中的 scrollTo 函式會被呼叫(onTouchEvent 呼叫 scrollBy,scrollBy 呼叫 scrollTo),它會根據使用者 touch move 移動的距離來更改當前頁面的角度,即變數 currentDegree 的值。具體請看如下程式碼:

清單 8

  1. public void scrollTo(int x, int y)   
  2. {   
  3.     if (getScrollX() != x || getScrollY() != y)   
  4.     {   
  5.     int oldX = getScrollX();   
  6.     int oldY = getScrollY();   
  7.   
  8.     super.scrollTo(x, y);   
  9.     //x is the touch action X direction move distance   
  10.     currentDegree = x * degreeOffset;              
  11.     onScrollChanged(x, y, oldX, oldY);   
  12.     invalidate();   
  13.     }   
  14. }   


 

這個立方體特效部分的程式碼介紹到這裡。

結束語

本文介紹了 Android launcher 的平滑和立體翻頁效果實現,可以幫助開發者深入理解 Android 的動畫框架原理,從而能夠充分利用 android 現有框架來做出夠眩、夠酷的動畫效果。


參考資料

學習

討論

作者簡介

 

朱韋偉 , IBM 中國系統與科技開發中心 HPC 部門的一名軟體工程師,熟悉嵌入式開發。

李浩 , 軟體工程師 , 北京愛格碼科技有限公司,從事 Android 平臺上的驅動以及應用程式開發。

 

本文轉自 http://www.ibm.com/developerworks/cn/opensource/os-cn-android-anmt2/index.html?ca=drs-

相關文章