衛星菜單是現在一個非常受歡迎的“控件”,很多Android程序員都趨之若鶩,預覽如下圖。傳統的衛星菜單是用Animation實現的,需要大量的代碼,而且算法極多,一不小心就要通宵Debug。本帖貼出用屬性動畫Animator來實現衛星菜單。
一、淺析屬性動畫Animator
Animator是Android3.0發布的新功能,代碼簡單,效果豐富。屬性動畫,顧名思義,只要是可以GET和SET的屬性,我們都可以用屬性動畫進行處理。屬性動畫中常用的屬性和方法如下:
ValueAnimator //數值發生器,可以實現很多很靈活的動畫效果 ObjectAnimator //ValueAnimator的子類,對ValueAnimator進行了封裝,讓我們可以更輕松的使用屬性動畫,我們通過ObjectAnimator來操縱一個對象,產生動畫效果 AnimatorListener //對動畫的開始、結束、暫停、重復等動作的事件監聽(需要重寫四個方法) AnimatorListenerAdapter //對動畫的開始、結束、暫停、重復中的一個動作的事件監聽(根據選擇的動作,只需要重寫一個方法) AnimatorSet //動畫的集合,用來設置多個動畫之間的關系(之前、之后、同時等) PropertyValuesHolder //動畫的集合,和AnimatorSet類似 TypeEvaluator //值計算器,在使用ValueAnimator.ofObject()方法時引入自定義的屬性對象 Interpolator //插值器,設置動畫的特效(速度漸變、彈跳等)
下面對這幾個類做一下簡單的介紹:
(一)ObjectAnimator:ObjectAnimator是最簡單、最常用的屬性動畫,根據文檔上的敘述:?This subclass of ValueAnimator provides support for animating properties on target objects.?,這個ValueAnimator的子類對Object對象的屬性提供動畫。ObjectAnimator中常用的屬性如下:
translationX / translationY 水平/垂直平移 rotaionX / rotationY 橫向/縱向旋轉 scaleX / scaleY 水平/垂直縮放 X / Y 直接到達X/Y坐標 alpha 透明度
我們使用一些方法(?ofFloat()?、?ofInt()?、?ofObject()?、?ofPropertyValuesHolder()?等)來實現動畫,調用?start()?方法來啟動動畫。選擇哪個方法,主要是屬性值的類型決定的。我們先來看看文檔中對這幾個方法的介紹:
target指的是動畫的作用對象,一般指控件;propertyName就是上面說的“translationX”、“alpha”等屬性名;values是一個不定長數組,記錄著屬性的始末值。唯一不同的是ofPropertyValuesHolder()方法,這個方法沒有property參數,是因為這個參數在PropertyValuesHolder對象中就已經使用(下面會介紹PropertyValuesHolder的使用方法)。
(二)AnimatorListener:這是一個接口,監聽屬性動畫的狀態(開始/重復/結束/取消),Animator及其子類(包括ValueAnimator和ObjectAnimator等)都可以使用這個接口,使用?anim.addListener()?使用這個接口。具體的使用方法如下:
animator.addListener(new AnimatorListener() {@Override // 動畫開始的監聽器public void onAnimationStart(Animator animation) { ... }@Override // 動畫重復的監聽器public void onAnimationRepeat(Animator animation) { ... }@Override // 動畫結束的監聽器public void onAnimationEnd(Animator animation) { ... }@Override // 動畫取消的監聽器public void onAnimationCancel(Animator animation) { ... } });
(三)AnimatorListenerAdapter:我們在實際開發中往往不會用到AnimatorListener中的全部四個方法,所以如果我們使用AnimatorListener,不僅浪費系統資源,代碼看上去也不好看,因此,我們需要一個適配器(Adapter),可以讓我們根據自己的意愿,只實現監聽器中的一個或幾個方法,這就要用到AnimatorListenerAdapter了。AnimatorListenerAdapter的使用方法和AnimatorListener一樣,都需要使用?anim.addListener()?方法,不同的是參數。代碼如下:
animator.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) { ... }@Overridepublic void onAnimationCancel(Animator animation) { ... } });
(四)AnimatorSet:先來看文檔中的介紹:?This class plays a set of Animator objects in the specified order. Animations can be set up to play together, in sequence, or after a specified delay. ?這個類支持按順序播放一系列動畫,這些動畫可以同時播放、按順序播放,也可以在一段時間之后播放(主要通過?setStartDelay()?方法實現)。下面是文檔中對這些方法的介紹:
值得說的是play()方法,它返回的是一個Builder對象,而Builder對象可以通過?with()?、?before()?、?after()?等方法,非常方便的控制一個動畫與其他Animator動畫的先后順序。例如:
AnimatorSet s = new AnimatorSet(); s.play(anim1).with(anim2); s.play(anim2).before(anim3); s.play(anim4).after(anim3);
(五)PropertyValuesHolder:使用PropertyValuesHolder結合ObjectAnimator或ValueAnimator,可以達到AnimatorSet的效果。可以說,PropertyValuesHolder就是為了動畫集而存在的,它不能單獨的存在,因為它沒有target參數(因此可以節省系統資源),所以只能依靠ObjectAnimator或ValueAnimator存在。一個實例的代碼如下:
PropertyValuesHolder pvh1 = PropertyValuesHolder.ofFloat("translationX", 0F, 200F); PropertyValuesHolder pvh2 = PropertyValuesHolder.ofFloat("translationY", 0F, 200F); PropertyValuesHolder pvh3 = PropertyValuesHolder.ofFloat("rotation", 0F, 360F); ObjectAnimator.ofPropertyValuesHolder(top, pvh1, pvh2, pvh3).setDuration(2000).start();
(六)ValueAnimator:是ObjectAnimator的父類,但由于不能相應動畫,也不能設置屬性(值的是沒有target和property兩個參數),所以不常用。如果我們要監聽ValueAnimator,則只能為ValueAnimator添加一個AnimatorUpdateListener(AnimatorUpdateListener可以監聽Animator每個瞬間的變化,取出對應的值)。一個實例的代碼如下:
ValueAnimator animator = ValueAnimator.ofInt(0, 100); // 沒有target和property參數 animator.setDuration(5000); animator.addUpdateListener(new AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {Integer value = (Integer) animation.getAnimatedValue();System.out.println("-->" + value);} }); animator.start();
(七)TypeEvaluator:主要用于ValueAnimator的ofObject()方法。根據文檔中的介紹,?Evaluators allow developers to create animations on arbitrary property types?,Evaluators允許開發者自定義動畫參數。因此,使用TypeEvaluator,我們可以打破ObjectAnimator的動畫范圍禁錮,創造我們自己的動畫。大致代碼如下:
ValueAnimator animator = ValueAnimator.ofObject(new TypeEvaluator<Object>() {@Overridepublic Object evaluate(float fraction, Object startValue, Object endValue) {return null;} }); animator.start();
(八)Interpolator:插值器,可以設置動畫的效果。Animator內置了很多插值器,如漸加速、漸減速、彈跳等等。插值器的種類可以在模擬器API DEMO應用的Views - Animation - Interpolator中查看。為Animator添加插值器只需要?animator.setInterpolator(new OvershootInterpolator());?即可。
?
二、實現衛星菜單
實現衛星菜單,我們主要使用了ObjectAnimator。先來介紹一下這個DEMO的效果(如下組圖):開始的時候在屏幕左上角顯示一個紅色的按鈕,點擊之后彈出一列子菜單,每個子菜單之間的距離遞增,彈出的時間遞減,彈出時會有OverShoot效果,點擊每個子菜單都會彈出相應的Toast;彈出菜單后再次點擊紅色按鈕,則收回所有子菜單。
下面貼出代碼。


1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:background="@color/white" > 6 7 <!-- 上面的七張圖片是七個子菜單 --> 8 <ImageView 9 android:id="@+id/menuitem_07" 10 style="@style/SateliteMenu_Item_Theme" 11 android:contentDescription="@string/app_name" 12 android:src="@drawable/item_07" /> 13 14 <ImageView 15 android:id="@+id/menuitem_06" 16 style="@style/SateliteMenu_Item_Theme" 17 android:contentDescription="@string/app_name" 18 android:src="@drawable/item_06" /> 19 20 <ImageView 21 android:id="@+id/menuitem_05" 22 style="@style/SateliteMenu_Item_Theme" 23 android:contentDescription="@string/app_name" 24 android:src="@drawable/item_05" /> 25 26 <ImageView 27 android:id="@+id/menuitem_04" 28 style="@style/SateliteMenu_Item_Theme" 29 android:contentDescription="@string/app_name" 30 android:src="@drawable/item_04" /> 31 32 <ImageView 33 android:id="@+id/menuitem_03" 34 style="@style/SateliteMenu_Item_Theme" 35 android:contentDescription="@string/app_name" 36 android:src="@drawable/item_03" /> 37 38 <ImageView 39 android:id="@+id/menuitem_02" 40 style="@style/SateliteMenu_Item_Theme" 41 android:contentDescription="@string/app_name" 42 android:src="@drawable/item_02" /> 43 44 <ImageView 45 android:id="@+id/menuitem_01" 46 style="@style/SateliteMenu_Item_Theme" 47 android:contentDescription="@string/app_name" 48 android:src="@drawable/item_01" /> 49 50 <!-- 最上層的紅色按鈕,因為它顯示在最上面,因此布局代碼在最后 --> 51 <ImageView 52 android:id="@+id/menu_top" 53 android:layout_width="35.0dip" 54 android:layout_height="35.0dip" 55 android:layout_marginLeft="17.0dip" 56 android:layout_marginTop="17.0dip" 57 android:contentDescription="@string/app_name" 58 android:src="@drawable/top_toopen" /> 59 60 </RelativeLayout>


1 public class MainActivity extends Activity implements OnClickListener { 2 private ImageView top; // 紅色按鈕 3 // 七個子菜單的ID組成的數組 4 private int[] ids = new int[] { R.id.menuitem_01, R.id.menuitem_02, R.id.menuitem_03, R.id.menuitem_04, R.id.menuitem_05, R.id.menuitem_06, R.id.menuitem_07, }; 5 private List<ImageView> itemList; // 七個子菜單都加到List中 6 private boolean isMenuOpen; // 記錄子菜單是否打開了 7 8 @Override 9 protected void onCreate(Bundle savedInstanceState) { 10 super.onCreate(savedInstanceState); 11 setContentView(R.layout.activity_main); 12 initView(); 13 } 14 15 private void initView() { 16 top = (ImageView) findViewById(R.id.menu_top); 17 top.setOnClickListener(this); 18 19 itemList = new ArrayList<ImageView>(); 20 for (int i = 0; i < ids.length; i++) { 21 ImageView imageView = (ImageView) findViewById(ids[i]); 22 imageView.setOnClickListener(this); 23 itemList.add(imageView); 24 } 25 } 26 27 @Override 28 public void onClick(View v) { 29 switch (v.getId()) { 30 case R.id.menu_top: 31 if (!isMenuOpen) { // 如果子菜單處于關閉狀態,則使用Animator動畫打開菜單 32 for (int i = 0; i < itemList.size(); i++) { 33 // 子菜單之間的間距對著距離的增加而遞增 34 ObjectAnimator animator = ObjectAnimator.ofFloat(itemList.get(i), "translationY", 0, (i + 1) * (30 + 2 * i)); 35 animator.setDuration((7 - i) * 100); // 最遠的子菜單彈出速度最快 36 animator.setInterpolator(new OvershootInterpolator()); // 設置插值器 37 animator.start(); 38 } 39 top.setImageResource(R.drawable.top_toclose); 40 isMenuOpen = true; 41 } else { // 如果子菜單處于打開狀態,則使用Animator動畫關閉菜單 42 for (int i = 0; i < itemList.size(); i++) { 43 ObjectAnimator animator = ObjectAnimator.ofFloat(itemList.get(i), "translationY", (i + 1) * (30 + 2 * i), 0); 44 animator.setDuration((7 - i) * 100); 45 animator.start(); 46 } 47 top.setImageResource(R.drawable.top_toopen); 48 isMenuOpen = false; 49 } 50 break; 51 default: 52 Toast.makeText(MainActivity.this, "Item" + (itemList.indexOf(v) + 1) + " Clicked...", Toast.LENGTH_SHORT).show(); 53 break; 54 } 55 } 56 }
?
三、擴展
使用屬性動畫可以實現的功能還有很多,這里再追加一個“評分”的功能實現。直接顯示成績往往給人一種突兀的感覺,所以我們想利用屬性動畫來實現一個數字變換的“成績單”,讓成績滾動顯示,同時,越到最后滾動的越慢,最后停止在真正的分數上。
思路:我們需要用到ValueAnimator,并用AnimatorUpdateListener作為動畫的監聽器,監聽動畫的每一個動作,并為成績的TextView設置Text。
以下是代碼。


1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:background="@color/white" > 6 7 <TextView 8 android:id="@+id/main_mark" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" 11 android:layout_centerInParent="true" 12 android:textColor="#ff0000" 13 android:textSize="65.0sp" /> 14 15 </RelativeLayout>


1 public class MainActivity extends Activity { 2 private TextView mark; 3 private static final int REAL_MARK = 96; 4 5 @Override 6 protected void onCreate(Bundle savedInstanceState) { 7 super.onCreate(savedInstanceState); 8 setContentView(R.layout.activity_main); 9 initView(); 10 } 11 12 private void initView() { 13 mark = (TextView) findViewById(R.id.main_mark); 14 15 ValueAnimator animator = ValueAnimator.ofInt(0, REAL_MARK); 16 animator.setDuration(3000); 17 animator.setInterpolator(new DecelerateInterpolator()); 18 animator.addUpdateListener(new AnimatorUpdateListener() { 19 @Override 20 public void onAnimationUpdate(ValueAnimator animation) { 21 Integer value = (Integer) animation.getAnimatedValue(); 22 mark.setText(value + ""); 23 } 24 }); 25 animator.start(); 26 } 27 }
?