卫星菜单可能网上已经有很多博文了,but,这里仅记录下自己的学习路程~刚看到自定义卫星菜单的时候真的是一脸懵逼,看完所有的源码觉得还可以接受,自己写难度较大,功力太薄呜呜。这个还是学习蛮不错的实例,涉及到动画,自定义的ViewGroup,接口,如何全面的考虑问题等等,最重要的是 思想!
一:自定义的ViewGroup
自定义属性
a.编写attrs.xml
b.在布局文件中使用
c.在自定义控件中获取属性
对子view的测量 onMeasure()
1.属性文件 res->value->attrs.xml
<’declare-styleable name=”NAME”>
<’attr name=”” format=”string/dimension/color/reference”/>
………
<’/declare-styleable>
2.在构造方法中用代码来获取在attr.xml文件中自定义的那些属性
TypeArray ta=context.obtainStyledAttributes(attrs,R.Styleable.NAME);
3.通过ta.getColor(),getString()…来获取这些定义的属性值
4.ta.recycle()
获取玩所以得属性值后,一般调用recycle方法来避免重新创建的时候的错误
a、attrs.xml
<resources>
<attr name="position" >
<enum name="left_top" value="0"/>
<enum name="left_bottom" value="1"/>
<enum name="right_top" value="2"/>
<enum name="right_bottom" value="3"/>
attr>
<attr name="radius" format="dimension"/>
<declare-styleable name="ArcMenu">
<attr name="position"/>
<attr name="radius"/>
declare-styleable>
resources>
新建一个menu_right_bottom_layout.xml
就是一个主按钮和几个菜单按钮
<com.calolin.animationtrain.view.ArcMenu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:calolin="http://schemas.android.com/apk/res-auto"
android:id="@+id/arcmenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
calolin:position="right_bottom"
calolin:radius="150dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/a">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/composer_icn_plus"/>
RelativeLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/b"
android:tag="Camera"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/c"
android:tag="Music"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/d"
android:tag="Location"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/e"
android:tag="Sleep"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/f"
android:tag="People"/>
com.calolin.animationtrain.view.ArcMenu>
你可以在这里自定义半径的大小,子菜单的个数,菜单的位置(上下左右)
b、在主布局文件中调用
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:calolin="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/menu_right_bottom_layout"/>
RelativeLayout>
接下来是最重要的自定义view文件ArcMenu.java
首先要定义一些变量,比如菜单的半径,位置(上下左右),控制菜单打开关闭的主按钮,菜单当前的状态等
/**
* 菜单的位置
*/
private Position mposition =Position.RIGHT_BOTTOM;
private static final int LEFT_TOP=0;
private static final int LEFT_BOTTOM=1;
private static final int RIGHT_TOP=2;
private static final int RIGHT_BOTOM=3;
/**
* 菜单的半径
*/
private int mRadius;
/**
* 菜单的中心按钮
*/
private View mCenterBtn;
/**
* 菜单的当前状态
*/
private Status mCurrentStatus=Status.CLOSE;
private OnMenuItemClickListener menuItemClickListener;
/**
* 菜单的位置的枚举类型
*/
private enum Position{
LEFT_TOP,LEFT_BOTTOM,RIGHT_TOP,RIGHT_BOTTOM
};
/**
* 菜单的状态
*/
private enum Status{
OPEN,CLOSE
};
public interface OnMenuItemClickListener{
void onClick(View view,int pos);
}
public void setOnMenuItemClickListener(OnMenuItemClickListener menuItemClickListener) {
this.menuItemClickListener = menuItemClickListener;
}
这里还写了一个子菜单点击的回调接口,在主活动中可以通过回调实现它的具体点击内容。
接口的一般写法,只要改名称和方法即可:
1、写一个接口类
public interface onMenuItemClickListener(名称){
void onFinish(方法名)([参数]);
….;(多个方法)
}
2、实例一个接口
private onMenuItemClickListener mListener;
3、写set方法
public void setOnMenuItemClickListener(onMenuItemClickListener listener ){
this.mListener = listener;
}
4、在活动中实现接口回调
targetView.setOnMenuItemClickListener(new onMenuItemClickListener(名称){
void onFinish(方法名)([参数]){
//具体实现..
};
….(多个方法)
});
c、然后在它的构造方法中对自定义控件进行读取
public ArcMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/*
半径默认值 100dp,TypedValue.applyDimension() 是转变尺寸的函数,这里COMPLEX_UNIT_DIP是单位,20是 数值,也就是20dp。
*/
mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,100,getResources().getDisplayMetrics());
//获取自定义属性的值
TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.ArcMenu);
mRadius = (int) ta.getDimension(R.styleable.ArcMenu_radius,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,100,getResources().getDisplayMetrics()));
//这里要考虑主按钮的位置,根据布局文件传过来我们设置的位置,在这里赋 给mposion
int pos = ta.getInt(R.styleable.ArcMenu_position,RIGHT_BOTOM);
switch (pos){
case LEFT_TOP:
mposition = Position.LEFT_TOP;
break;
case LEFT_BOTTOM:
mposition = Position.LEFT_BOTTOM;
break;
case RIGHT_TOP:
mposition = Position.RIGHT_TOP;
break;
case RIGHT_BOTOM:
mposition = Position.RIGHT_BOTTOM;
break;
}
Log.e("TAG","position="+mposition+",radius="+mRadius);
ta.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i <'count; i++) {
//测量child
measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec);
}
}
有时候,当ViewGroup的宽和高是wrap_content的情况下,控件的宽和高要根据子view的宽和高去决定,要在onMeasure方法下做一些其他的操作,比如说遍历它的子view,获取它们的宽和高等,最后setMeasureDimension得到它最终的宽和高。这里由于控件都是全屏的(match_parent),不用根据子view去获得它的宽和高。
a、首先要确定主按钮的位置
/**
* 确定主按钮的位置
*/
private void layoutCenterBtn() {
mCenterBtn = getChildAt(0);//获取主按钮
mCenterBtn.setOnClickListener(this);//为其注册点击事件
int l=0;
int t=0;
int width = mCenterBtn.getMeasuredWidth();
int height = mCenterBtn.getMeasuredHeight();
switch (mposition){
case LEFT_TOP:
l = 0;
t = 0;
break;
case LEFT_BOTTOM:
l = 0;
t = getMeasuredHeight()-height;//屏幕的高度-按钮的高度
break;
case RIGHT_TOP:
l = getMeasuredWidth()-width;
t = 0;
break;
case RIGHT_BOTTOM:
l = getMeasuredWidth()-width;
t = getMeasuredHeight()-height;
break;
}
mCenterBtn.layout(l,t,l+width,t+height);
}
这里也是要考虑主按钮在上下左右四种情况。
b、为主按钮添加动画
/**
*主按钮旋转
*/
private void rotateCenterBtn(View v, float start, float end, int duration) {
//使按钮绕自身中心旋转360度
RotateAnimation anim = new RotateAnimation(start,end, Animation.RELATIVE_TO_SELF,
0.5f,Animation.RELATIVE_TO_SELF,0.5f);
anim.setDuration(duration);
anim.setFillAfter(true);//保持改变后的状态
v.startAnimation(anim);
}
c、确定子菜单的位置
//菜单的left、top
int cl = (int) (mRadius* Math.sin(Math.PI/2/(count-2)*i));
int ct = (int) (mRadius*Math.cos(Math.PI/2/(count-2)*i));
//获取菜单按钮的宽度和高度
int cWidth = child.getMeasuredWidth();
int cHeight = child.getMeasuredHeight();
//如果菜单在底部
if (mposition == Position.LEFT_BOTTOM||mposition == Position.RIGHT_BOTTOM){
ct = getMeasuredHeight()-cHeight-ct;
}
//如果菜单在右边
if (mposition == Position.RIGHT_TOP||mposition == Position.RIGHT_BOTTOM){
cl = getMeasuredWidth()-cWidth -cl;
}
child.layout(cl,ct,cl+cWidth,ct+cHeight);
}
}
这里的难点是确定子菜单的left和top的位置。以左上角为例,如果有四个子菜单,那么a=90/(菜单数-1);
menu i 的坐标为:radius*sin(i*a),radius*cos(i*a)
d、切换菜单
主按钮和子菜单的位置都确定了,接下来就是菜单的打开和关闭了。
/**
* 切换菜单
*/
public void toggleMenu(int duration)
{
// 为menuItem添加平移动画和旋转动画
int count = getChildCount();
for (int i = 0; i < count - 1; i++)
{
final View childView = getChildAt(i + 1);//1 ~ count个子菜单
childView.setVisibility(View.VISIBLE);
// end 0 , 0
// start
int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));
int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));
//当主按钮在不同位置时,菜单平移的增量可能为正,可能为负,这里要进行判断
int xflag = 1;
int yflag = 1;
if (mposition == Position.LEFT_TOP
|| mposition == Position.LEFT_BOTTOM)
{
xflag = -1;
}
if (mposition == Position.LEFT_TOP
|| mposition == Position.RIGHT_TOP)
{
yflag = -1;
}
AnimationSet animset = new AnimationSet(true);
Animation tranAnim = null;
// to open
//子菜单的位置为0,0 ,只是设置为不可见,所以打开菜单时,是从四个角落里移动到原来的位置
//如果是菜单的状态是关闭的,就让它打开
if (mCurrentStatus == Status.CLOSE)
{
tranAnim = new TranslateAnimation(xflag * cl, 0, yflag * ct, 0);
childView.setClickable(true);
childView.setFocusable(true);
} else
// to close
{
tranAnim = new TranslateAnimation(0, xflag * cl, 0, yflag * ct);
childView.setClickable(false);
childView.setFocusable(false);
}
tranAnim.setFillAfter(true);
tranAnim.setDuration(duration);
tranAnim.setStartOffset((i * 100) / count);//设置出场的偏移量,让所有的子菜单在很短的时间内有序弹出来
tranAnim.setAnimationListener(new Animation.AnimationListener()
{
@Override
public void onAnimationStart(Animation animation)
{ }
@Override
public void onAnimationRepeat(Animation animation)
{ }
@Override
public void onAnimationEnd(Animation animation)
{
if (mCurrentStatus == Status.CLOSE)
{
childView.setVisibility(View.GONE);
}
}
});
// 旋转动画
RotateAnimation rotateAnim = new RotateAnimation(0, 720,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnim.setDuration(duration);
rotateAnim.setFillAfter(true);
animset.addAnimation(rotateAnim);
animset.addAnimation(tranAnim);
childView.startAnimation(animset);
final int pos = i + 1;
childView.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
if (menuItemClickListener != null)
menuItemClickListener.onClick(childView, pos);//在主活动中会重写这个方法
menuItemAnim(pos - 1);//为菜单添加动画
changeStatus();//这里若是点击了某个菜单项,要改变菜单的状态,如果状态是打开的要将其关闭。
}
});
}
// 切换菜单状态
changeStatus();
}
把切换菜单的源码附上:
/**
* 切换菜单状态
*/
private void changeStatus() {
mCurrentStatus = (mCurrentStatus == Status.CLOSE?Status.OPEN:Status.CLOSE);
}
e、剩下的就是菜单的动画,被点击的菜单变大,透明度降低,其他菜单变小,透明度降低,比较容易理解。主要掌握AnimationSet和视图动画的巧妙运用。
/**
* 添加menuItem的点击动画
*/
private void menuItemAnim(int pos)
{
for (int i = 0; i < getChildCount() - 1; i++)
{
View childView = getChildAt(i + 1);
if (i == pos)
{
childView.startAnimation(scaleBigAnim(300));
} else
{
childView.startAnimation(scaleSmallAnim(300));
}
childView.setClickable(false);
childView.setFocusable(false);
}
}
private Animation scaleSmallAnim(int duration)
{
AnimationSet animationSet = new AnimationSet(true);
ScaleAnimation scaleAnim = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
AlphaAnimation alphaAnim = new AlphaAnimation(1f, 0.0f);
animationSet.addAnimation(scaleAnim);
animationSet.addAnimation(alphaAnim);
animationSet.setDuration(duration);
animationSet.setFillAfter(true);
return animationSet;
}
/**
* 为当前点击的Item设置变大和透明度降低的动画
*
* @param duration
* @return
*/
private Animation scaleBigAnim(int duration)
{
AnimationSet animationSet = new AnimationSet(true);
ScaleAnimation scaleAnim = new ScaleAnimation(1.0f, 4.0f, 1.0f, 4.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
AlphaAnimation alphaAnim = new AlphaAnimation(1f, 0.0f);
animationSet.addAnimation(scaleAnim);
animationSet.addAnimation(alphaAnim);
animationSet.setDuration(duration);
animationSet.setFillAfter(true);
return animationSet;
}
f、最后只剩下在活动中测试了,咳。
public class MainActivity extends AppCompatActivity {
private ArcMenu arcmenu;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
arcmenu = (ArcMenu) findViewById(R.id.arcmenu);
arcmenu.setOnMenuItemClickListener(new ArcMenu.OnMenuItemClickListener() {
@Override
public void onClick(View view, int pos) {
Toast.makeText(MainActivity.this,pos+":"+view.getTag(),Toast.LENGTH_SHORT).show();
}
});
}
}
你可以在主布局文件中添加其他的view,比如listview。感谢洪洋大神的视频
最后,来两张截图: