從網上學習了hyman大神的衛星菜單實現,自己特意親自又寫了一編代碼,對自定義ViewGroup的理解又深入了一點。我堅信只有自己寫出來的知識才會有更加好的的掌握。因此也在自己的博客中將這個衛星菜單的案例給實現了。廢話不多說,直接進入正題吧。
該案例的完整代碼以及相關的圖片素材下載鏈接如下:
http://download.csdn.net/detail/fuyk12/9233573
一、效果展示
首先上一張效果圖,如下:
這是在模擬器上的運行效果,有點卡頓。在真機上,是會很流暢,而且動畫效果顯示的會更清晰。
效果說明:紅色按鈕是菜單按鈕,點擊它,會彈出菜單選項。菜單選項會像一個四分之一圓一樣圍繞在菜單按鈕周圍。而菜單的彈出采用了動畫效果。如果點擊菜單,則同時菜單消失且會彈出一個提示框。更具體的細節效果,看上面的效果圖應該很明白了。
所用的知識:(1)自定義ViewGroup。整個菜單的繪制就是一個自定義的ViewGroup。所以這其中要清楚理解view的繪制流程,還要掌握住自定義屬性等知識。
(2)android中的常用的補間動畫,例如菜單的彈出就是平移動畫和旋轉動畫的結合。而點擊菜單時,是縮放動畫和透明動畫的實現。
難點:我們android的補間動畫實際上視覺上的一種錯覺,比如一個按鈕你看到它平移了,實際上它只是android在另外一個地方重新繪制了,並不是原來的按鈕,原來的按鈕還 在初始位置,因此你點擊它是沒有效果的。為了解決這個問題,android推出了屬性動畫。但是屬性動畫向下兼容性不是很好。為了解決這樣子的難點,我們依舊采用補 間 動畫,但是在邏輯上做了一個巧妙的處理。具體的怎么處理,等到了這一步,再詳細說明。
實現這個案例的思路:我們打算分這么幾步來實現這個衛星菜單。
(1)首先將各個菜單以及按鈕給完整的繪制出來。即完成自定義ViewGroup。
(2)完成菜單的彈出動畫。
(3)完成菜單的點擊動畫。
(4)完成菜單點擊事件的邏輯,比如彈出提示框。
因此這是一個實戰的案例,不是基礎知識的講解。需要讀者具有相關的知識后才能很好的閱讀。那么本篇文章就來實現第一步,即將整個菜單給繪制出來。
二、自定義ViewGruop完成菜單繪制
從菜單按鈕以及菜單選項,都是一個自定義的ViewGroup。從效果圖上可以看到,菜單選項以一個四分之一圓的方式圍繞菜單。因此這個自定義的ViewGroup需要具有圓的半徑這個屬性。效果圖中只展示了按鈕位於右下角的情況,其實那個加號按鈕完全可以放在左上,左下,右上。因此,我們給這個自定義的ViewGroup再添加一個屬性,為位置屬性。這樣子一來,用戶就可以放在四個角的其他位置了。這些分析,其實就是自定義屬性里面的內容。表現在代碼中,如下所示。
新建項目,然后再res下的values文件下新建attrs.xml文件,其中的代碼如下:
1 <?xml version="1.0" encoding="utf-8"?> 2 <resources> 3 4 <attr name="position"> 5 <enum name="left_top" value= "0"/> 6 <enum name="left_bottom" value= "1"/> 7 <enum name="right_top" value= "2"/> 8 <enum name="right_bottom" value= "3"/> 9 </attr> 10 <attr name="radius" format="dimension"/> 11 12 <declare-styleable name = "ArcMenu"> 13 <attr name="position"/> 14 <attr name="radius"/> 15 </declare-styleable> 16 17 </resources>
從代碼中,可以看到我們定義了一個屬性集合“ArcMenu",其中有兩個屬性"position"和"radius”,對應於自定義ViewGruop的位置和需要的半徑。其實ArcMenu就是我們自定義ViewGroup的名稱。我們先將其建立出來。
新建類ArcMenu繼承自ViewGroup,代碼如下:
1 package com.example.menu; 2 3 import android.content.Context; 4 import android.content.res.TypedArray; 5 import android.util.AttributeSet; 6 import android.util.TypedValue; 7 import android.view.View; 8 import android.view.View.OnClickListener; 9 import android.view.animation.Animation; 10 import android.view.animation.RotateAnimation; 11 import android.view.ViewGroup; 12 13 public class ArcMenu extends ViewGroup { 14 15 16 public ArcMenu(Context context) { 17 this(context,null); 18 } 19 public ArcMenu(Context context, AttributeSet attrs) { 20 this(context,attrs,0); 21 } 22 public ArcMenu(Context context, AttributeSet attrs, int defStyle) { 23 super(context, attrs, defStyle); 24 25 } 26 27 }
代碼很簡單,我們沒有做任何操作。下面要做的就是把這個自定義的ViewGroup添加到布局中,並向里面添加控件,即我們的菜單。
修改activity_main.xml中的代碼,如下:
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 xmlns:kun="http://schemas.android.com/apk/res/com.example.menu" 4 android:layout_width="match_parent" 5 android:layout_height="match_parent" 6 android:orientation="vertical" > 7 8 <com.example.menu.ArcMenu 9 android:layout_width="match_parent" 10 android:layout_height="match_parent" 11 kun:position="right_bottom" 12 kun:radius="200dp"> 13 <RelativeLayout 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" 16 android:background="@drawable/composer_button" > 17 <ImageView 18 android:id="@+id/id_button" 19 android:layout_width="wrap_content" 20 android:layout_height="wrap_content" 21 android:layout_centerInParent="true" 22 android:src="@drawable/composer_icn_plus"/> 23 </RelativeLayout> 24 <ImageView 25 android:layout_width="wrap_content" 26 android:layout_height="wrap_content" 27 android:src="@drawable/composer_camera" 28 android:tag="Camera"/> 29 <ImageView 30 android:layout_width="wrap_content" 31 android:layout_height="wrap_content" 32 android:src="@drawable/composer_music" 33 android:tag="Music"/> 34 <ImageView 35 android:layout_width="wrap_content" 36 android:layout_height="wrap_content" 37 android:src="@drawable/composer_place" 38 android:tag="Place"/> 39 <ImageView 40 android:layout_width="wrap_content" 41 android:layout_height="wrap_content" 42 android:src="@drawable/composer_sleep" 43 android:tag="Sleep"/> 44 <ImageView 45 android:layout_width="wrap_content" 46 android:layout_height="wrap_content" 47 android:src="@drawable/composer_thought" 48 android:tag="Thought"/> 49 <ImageView 50 android:layout_width="wrap_content" 51 android:layout_height="wrap_content" 52 android:src="@drawable/composer_with" 53 android:tag="People"/> 54 55 </com.example.menu.ArcMenu> 56 57 58 </LinearLayout>
大體是一個線性布局。然后在里面放入了自定義的ViewGroup,即ArcMenu。注意在第11行和第12行我們為ArcMenu指定了位置為右下,半徑為200dp。你可以完全指定不同的位置和大小。而且我們在ArcMenu里面放入了好幾個ImageView,這些ImageView就是我們的菜單,至於圖片資源可以單擊文章開頭的鏈接下載。ImageView的個數,你完全可以自己來定,在這里我們放入了6個,,也就是有6個菜單選項。並且為每一個ImageView都指定了tag。這有什么用呢,先不用管它。你或許會有疑問,這些ImageView是怎么擺放的啊?所以接下來,趕快修改ArcMenu中的代碼,將這個控件都擺放好。
ArcMenu中的代碼先貼出來吧,然后再解釋,如下:
1 package com.example.menu; 2 3 import android.content.Context; 4 import android.content.res.TypedArray; 5 import android.util.AttributeSet; 6 import android.util.TypedValue; 7 import android.view.View; 8 import android.view.View.OnClickListener; 9 import android.view.animation.Animation; 10 import android.view.animation.RotateAnimation; 11 import android.view.ViewGroup; 12 13 public class ArcMenu extends ViewGroup implements OnClickListener { 14 /** 15 * 菜單按鈕 16 */ 17 private View mCBMenu; 18 /** 19 * 菜單的位置,為枚舉類型 20 * @author fuly1314 21 * 22 */ 23 private enum Position 24 { 25 LEFT_TOP,LEFT_BOTTOM,RIGHT_TOP,RIGHT_BOTTOM 26 } 27 /** 28 * 菜單的狀態 29 * @author fuly1314 30 * 31 */ 32 private enum Status 33 { 34 OPEN,CLOSE 35 } 36 /** 37 * 菜單為當前位置,默認為RIGHT_BOTTOM,在后面我們可以獲取到 38 */ 39 private Position mPosition = Position.RIGHT_BOTTOM; 40 /** 41 * 菜單的當前狀態,默認為開啟 42 */ 43 private Status mCurStatus = Status.OPEN; 44 45 /** 46 * 菜單的半徑,默認為120dp 47 */ 48 private int mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150, 49 getResources().getDisplayMetrics()); 50 51 52 53 public ArcMenu(Context context) { 54 this(context,null); 55 } 56 public ArcMenu(Context context, AttributeSet attrs) { 57 this(context,attrs,0); 58 } 59 public ArcMenu(Context context, AttributeSet attrs, int defStyle) { 60 super(context, attrs, defStyle); 61 62 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ArcMenu, defStyle, 0); 63 //獲取到菜單設置的位置 64 int position = ta.getInt(R.styleable.ArcMenu_position, 3); 65 66 switch(position){ 67 case 0: 68 mPosition = Position.LEFT_TOP; 69 break; 70 case 1: 71 mPosition = Position.LEFT_BOTTOM; 72 break; 73 case 2: 74 mPosition = Position.RIGHT_TOP; 75 break; 76 case 3: 77 mPosition = Position.RIGHT_BOTTOM; 78 break; 79 } 80 81 //獲取到菜單的半徑 82 mRadius = (int) ta.getDimension(R.styleable.ArcMenu_radius, 83 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 120, 84 getResources().getDisplayMetrics())); 85 ta.recycle(); 86 87 } 88 89 90 91 /** 92 * 測量各個子View的大小 93 */ 94 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 95 { 96 int count = getChildCount();//獲取子view的數量 97 98 for(int i=0;i<count;i++) 99 { 100 //測量子view的大小 101 measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec); 102 } 103 104 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 105 } 106 107 /** 108 * 擺放各個子view的位置 109 */ 110 protected void onLayout(boolean changed, int l, int t, int r, int b) { 111 112 if(changed)//如果發生了改變,就重新布局 113 { 114 layoutMainMenu();//菜單按鈕的布局 115 116 } 117 118 119 } 120 /** 121 * 菜單按鈕的布局 122 */ 123 private void layoutMainMenu() { 124 125 mCBMenu = getChildAt(0);//獲得主菜單按鈕 126 127 mCBMenu.setOnClickListener(this); 128 129 int left=0; 130 int top=0; 131 132 switch(mPosition) 133 { 134 case LEFT_TOP: 135 left = 0; 136 top = 0; 137 break; 138 case LEFT_BOTTOM: 139 left = 0; 140 top = getMeasuredHeight() - mCBMenu.getMeasuredHeight(); 141 break; 142 case RIGHT_TOP: 143 left = getMeasuredWidth() - mCBMenu.getMeasuredWidth(); 144 top = 0; 145 break; 146 case RIGHT_BOTTOM: 147 left = getMeasuredWidth() - mCBMenu.getMeasuredWidth(); 148 top = getMeasuredHeight() - mCBMenu.getMeasuredHeight(); 149 break; 150 } 151 152 mCBMenu.layout(left, top, left+mCBMenu.getMeasuredWidth(), top+mCBMenu.getMeasuredHeight()); 153 } 154 /** 155 * 菜單按鈕的點擊事件 156 * @param v 157 */ 158 public void onClick(View v) { 159 //為菜單按鈕設置點擊動畫 160 RotateAnimation rAnimation = new RotateAnimation(0f, 720f, Animation.RELATIVE_TO_SELF, 0.5f, 161 Animation.RELATIVE_TO_SELF, 0.5f); 162 163 rAnimation.setDuration(300); 164 165 rAnimation.setFillAfter(true); 166 167 v.startAnimation(rAnimation); 168 169 } 170 171 }
代碼有點長,但是很簡單。首先是成員變量,我們需要知道當前菜單按鈕的位置,是放在右下了還是左上,還需要當前菜單的狀態,菜單已經折疊了還是已經打開了,更需要知道半徑,這樣才好設定菜單的位置啊,因此才有了這些成員變量。再然后就是在構造方法中獲取我們在布局中的給ArcMenu設定的位置屬性和半徑屬性,從而將成員變量初始化。
接着在onMeasure方法,對放進ArcMenu中的每個子view進行測量,這樣子就知道了每一個子view的大小,代碼很簡單,measureChild一下即可。然后就可以執行onLayout方法來擺放每一個子view的位置了。這里我們先擺放菜單按鈕的位置,因為它比較簡單。菜單,即布局中的ImageView先不管。然后又給菜單按鈕添加了點擊事件,即點擊的時候,要它自身旋轉一下。好了,代碼很簡單,看注釋很容易理解。我們運行下,看看效果。如下:
右下角有個紅色的按鈕,點擊還有旋轉(由於貼出來的不是動態圖,因此旋轉效果小伙伴可以在自己的實驗中看到)。好了,菜單按鈕我們布局算是成功了。下面就開始布局那些菜單吧。
關於這些菜單,即這些ImageView的布局,牽涉到簡單的數學知識。下面有一張我繪制的圖片,簡單來說明一下。
假設菜單按鈕放在左上角,圍繞它的菜單如上圖,紅色的圈圈為代表。其中線段AB就是我們所設定的半徑的長度,用radius來表示。現在我們如果想求A點的坐標(其中A是對應菜單的頂點坐標),根據簡單的數學知識,就會得到如下結果:
x = radius * cos(a)
y = radius * sin(a)
那么同樣的道理,對於第 i 個菜單,它的頂點坐標就如下可求:
x = radius * cos(a*i)
y = radius * sin(a*i)
好了,現在回到我們的程序中來。要知道,在代碼中,你要注意以下問題:
(1)遍歷的子view的坐標是從0開始的。
(2)遍歷的子view包括菜單按鈕,因為我們要出去它。
(3)a的值應該是90除以菜單個數
注意到上面的問題,再結合我們前面分析到的數學知識,很容易可以得到每一個子view的頂點坐標了,代碼如下:
1 for(int i=0;i<count-1;i++) 2 { 3 View childView = getChildAt(i+1);//注意這里過濾掉菜單按鈕,只要菜單選項view 4 5 int left = (int) (mRadius*Math.cos(Math.PI/2/(count-2)*i)); 6 int top = (int) (mRadius*Math.sin(Math.PI/2/(count-2)*i)); 7 8 switch(mPosition) 9 { 10 11 case LEFT_TOP: 12 break; 13 case LEFT_BOTTOM: 14 top = getMeasuredHeight() - top-childView.getMeasuredHeight(); 15 break; 16 case RIGHT_TOP: 17 left = getMeasuredWidth() - left-childView.getMeasuredWidth(); 18 break; 19 case RIGHT_BOTTOM: 20 left = getMeasuredWidth() - left-childView.getMeasuredWidth(); 21 top = getMeasuredHeight() - top-childView.getMeasuredHeight(); 22 break; 23 } 24 }
怎么會多出switch語句呢?這是因為比如菜單按鈕位於右下,則頂點坐標的高度應該是整體高度去掉控件本身的高度以及頂點高度。如下圖所示:
swtich的其他語句相信小伙伴仔細想想應該很快就會明白為什么這么計算了吧。道理是一樣的。拿個本子,用筆畫畫就知道了
講了那么多,我們就將上面我們所講的整合到代碼中。那么修改ArcMenu,擺放我們的菜單吧!代碼如下:
1 package com.example.menu; 2 3 import android.content.Context; 4 import android.content.res.TypedArray; 5 import android.util.AttributeSet; 6 import android.util.TypedValue; 7 import android.view.View; 8 import android.view.View.OnClickListener; 9 import android.view.animation.Animation; 10 import android.view.animation.RotateAnimation; 11 import android.view.ViewGroup; 12 13 public class ArcMenu extends ViewGroup implements OnClickListener{ 14 /** 15 * 菜單按鈕 16 */ 17 private View mCBMenu; 18 /** 19 * 菜單的位置,為枚舉類型 20 * @author fuly1314 21 * 22 */ 23 private enum Position 24 { 25 LEFT_TOP,LEFT_BOTTOM,RIGHT_TOP,RIGHT_BOTTOM 26 } 27 /** 28 * 菜單的狀態 29 * @author fuly1314 30 * 31 */ 32 private enum Status 33 { 34 OPEN,CLOSE 35 } 36 /** 37 * 菜單為當前位置,默認為RIGHT_BOTTOM,在后面我們可以獲取到 38 */ 39 private Position mPosition = Position.RIGHT_BOTTOM; 40 /** 41 * 菜單的當前狀態,默認為開啟 42 */ 43 private Status mCurStatus = Status.OPEN; 44 45 /** 46 * 菜單的半徑,默認為120dp 47 */ 48 private int mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150, 49 getResources().getDisplayMetrics()); 50 51 52 53 public ArcMenu(Context context) { 54 this(context,null); 55 } 56 public ArcMenu(Context context, AttributeSet attrs) { 57 this(context,attrs,0); 58 } 59 public ArcMenu(Context context, AttributeSet attrs, int defStyle) { 60 super(context, attrs, defStyle); 61 62 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ArcMenu, defStyle, 0); 63 //獲取到菜單設置的位置 64 int position = ta.getInt(R.styleable.ArcMenu_position, 3); 65 66 switch(position){ 67 case 0: 68 mPosition = Position.LEFT_TOP; 69 break; 70 case 1: 71 mPosition = Position.LEFT_BOTTOM; 72 break; 73 case 2: 74 mPosition = Position.RIGHT_TOP; 75 break; 76 case 3: 77 mPosition = Position.RIGHT_BOTTOM; 78 break; 79 } 80 81 //獲取到菜單的半徑 82 mRadius = (int) ta.getDimension(R.styleable.ArcMenu_radius, 83 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 120, 84 getResources().getDisplayMetrics())); 85 ta.recycle(); 86 87 } 88 89 90 91 /** 92 * 測量各個子View的大小 93 */ 94 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 95 { 96 int count = getChildCount();//獲取子view的數量 97 98 for(int i=0;i<count;i++) 99 { 100 //測量子view的大小 101 measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec); 102 } 103 104 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 105 } 106 107 /** 108 * 擺放各個子view的位置 109 */ 110 protected void onLayout(boolean changed, int l, int t, int r, int b) { 111 112 if(changed)//如果發生了改變,就重新布局 113 { 114 layoutMainMenu();//菜單按鈕的布局 115 /** 116 * 下面的代碼為菜單的布局 117 */ 118 int count = getChildCount(); 119 120 for(int i=0;i<count-1;i++) 121 { 122 View childView = getChildAt(i+1);//注意這里過濾掉菜單按鈕,只要菜單選項view 123 124 int left = (int) (mRadius*Math.cos(Math.PI/2/(count-2)*i)); 125 int top = (int) (mRadius*Math.sin(Math.PI/2/(count-2)*i)); 126 127 switch(mPosition) 128 { 129 130 case LEFT_TOP: 131 break; 132 case LEFT_BOTTOM: 133 top = getMeasuredHeight() - top-childView.getMeasuredHeight(); 134 break; 135 case RIGHT_TOP: 136 left = getMeasuredWidth() - left-childView.getMeasuredWidth(); 137 break; 138 case RIGHT_BOTTOM: 139 left = getMeasuredWidth() - left-childView.getMeasuredWidth(); 140 top = getMeasuredHeight() - top-childView.getMeasuredHeight(); 141 break; 142 } 143 144 childView.layout(left, top, left+childView.getMeasuredWidth(), 145 top+childView.getMeasuredHeight()); 146 } 147 } 148 149 150 } 151 /** 152 * 菜單按鈕的布局 153 */ 154 private void layoutMainMenu() { 155 156 mCBMenu = getChildAt(0);//獲得主菜單按鈕 157 158 mCBMenu.setOnClickListener(this); 159 160 int left=0; 161 int top=0; 162 163 switch(mPosition) 164 { 165 case LEFT_TOP: 166 left = 0; 167 top = 0; 168 break; 169 case LEFT_BOTTOM: 170 left = 0; 171 top = getMeasuredHeight() - mCBMenu.getMeasuredHeight(); 172 break; 173 case RIGHT_TOP: 174 left = getMeasuredWidth() - mCBMenu.getMeasuredWidth(); 175 top = 0; 176 break; 177 case RIGHT_BOTTOM: 178 left = getMeasuredWidth() - mCBMenu.getMeasuredWidth(); 179 top = getMeasuredHeight() - mCBMenu.getMeasuredHeight(); 180 break; 181 } 182 183 mCBMenu.layout(left, top, left+mCBMenu.getMeasuredWidth(), top+mCBMenu.getMeasuredHeight()); 184 } 185 /** 186 * 菜單按鈕的點擊事件 187 * @param v 188 */ 189 public void onClick(View v) { 190 //為菜單按鈕設置點擊動畫 191 RotateAnimation rAnimation = new RotateAnimation(0f, 720f, Animation.RELATIVE_TO_SELF, 0.5f, 192 Animation.RELATIVE_TO_SELF, 0.5f); 193 194 rAnimation.setDuration(300); 195 196 rAnimation.setFillAfter(true); 197 198 v.startAnimation(rAnimation); 199 200 } 201 202 }
注意紅色部分的代碼,這樣子我們就把每一個菜單擺放到位了。那么是不是呢?快運行一下程序,看看效果。如下:
效果還不錯哈。至此,ArcMenu每一個子view都繪制出來了。我們下面要出來的就是菜單的動畫效果了。保存好現在的代碼,快快進入下一節中吧。《實現菜單彈出動畫》