从网上学习了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都绘制出来了。我们下面要出来的就是菜单的动画效果了。保存好现在的代码,快快进入下一节中吧。《实现菜单弹出动画》