(一)自定義ViewGroup繪制出菜單


      從網上學習了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都繪制出來了。我們下面要出來的就是菜單的動畫效果了。保存好現在的代碼,快快進入下一節中吧。《實現菜單彈出動畫》

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM