今天在慕课网上看到一篇视频教程,Android实现卫星菜单,感觉效果非常炫酷,想到印象笔记添加笔记的菜单也可以通过这种方式来实现,点击添加笔记菜单按钮,便会弹出一系列的按钮用于添加不同的笔记。于是自己试着仿照印象笔记的菜单按钮,写出一个自定义的菜单控件。
先上一下效果图,看看完成后的效果如何
我们先准备一下自定义菜单所需要的资源。Google发布了Material Design Icons,正好可以被我们拿来用,下载地址Material Design icon合集 ,选出我们需要的图标拷贝到Android Studio里。
首先,我们需要编写自定义控件的属性。在values文件夹下添加attr.xml文件,代码如下:
<declare-styleable name="MyMenu">
<attr name="position">
<enum name="left_top" value="0"/>
<enum name="right_top" value="1"/>
<enum name="left_bottom" value="2"/>
<enum name="right_bottom" value="3"/>
</attr>
<attr name="interval" format="dimension"/>
</declare-styleable>
自定义属性中包含两个属性,第一个是position,记录菜单在屏幕中所处的位置,第二个是interval,记录菜单展开后,每个按钮之间的间隔。
在XML文件中使用自定义菜单之前,需要新建MyMenu类,让它继承ViewGroup。实现MyMenu类的构造方法和onLayout方法,确保不报错即可。具体代码请看下一节内容。
新建一个布局文件,Android Studio会自动引用命名空间,所以可以直接写我们的自定义控件。我在自定义控件中增加了一个主按钮ImageView,四个子按钮LinearLayout,具体代码如下所示:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent">
<com.phoenix.myapplication.MyMenu android:layout_width="match_parent" android:layout_height="match_parent" app:interval="100dp" app:position="right_bottom">
<ImageView android:id="@+id/id_mainButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_add_circle_black_48dp"/>
<LinearLayout android:id="@+id/id_item1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="item1">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="item1"/>
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_archive_grey600_48dp"/>
</LinearLayout>
<LinearLayout android:id="@+id/id_item2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="item2">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="item2"/>
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_backspace_grey600_48dp"/>
</LinearLayout>
<LinearLayout android:id="@+id/id_item3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="item3">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="item3"/>
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_block_grey600_48dp"/>
</LinearLayout>
<LinearLayout android:id="@+id/id_item4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="item4">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="item4"/>
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_content_copy_grey600_48dp"/>
</LinearLayout>
</com.phoenix.myapplication.MyMenu>
</RelativeLayout>
还记得上一节我们写的MyMenu类吗?现在我们来完善它。
需要设置每个菜单按钮的间隔,需要设置菜单的位置,这些都可以通过自定义属性的值获得。别忘了设置默认值,最后要recycle。
对每个子控件调用measureChild方法。
在onLayout方法里,首先需要定位主按钮的位置。定义layoutMainButton方法,判断主按钮在屏幕的哪个角落。
之后,根据主按钮的位置计算子按钮的位置。如果主按钮在顶部,则子按钮的y轴坐标逐渐增加;如果主按钮在顶部,则子按钮的y轴坐标逐渐减少。
在这里可以设置子按钮的Tag,方便日后进行判断操作。
主按钮点击事件,如果菜单处于打开状态,则关闭菜单;如果菜单处于关闭状态,则打开菜单。里面对子按钮增加了动画和动画监听器,在动画结束后设置子按钮是否可见。点击后,需要通过changeStatus方法改变菜单的状态。
在此方法里,通过定义的OnMenuItemClickListener接口,实现子按钮的点击事件。同时,如果回调接口不为0的话,需要实现回调接口里的方法(也可以不实现)。
子菜单的点击动画,我们点击的Item会有scaleBigAnim(变大,变透明)的动画,而其他Items会有scaleSmallAnim(变小,变透明)的动画。
MyMenu的完整代码如下所示
public class MyMenu extends ViewGroup implements OnClickListener {
//自定义菜单位置
private static final int POS_LEFT_TOP = 0;
private static final int POS_RIGHT_TOP = 1;
private static final int POS_LEFT_BOTTOM = 2;
private static final int POS_RIGHT_BOTTOM = 3;
private Position mPosition = Position.RIGHT_BOTTOM;//菜单位置
private int mInterval;//菜单间隔
private Status mCurrentStatus = Status.CLOSE;//菜单状态
private View mMainButton;//主按钮
private OnMenuItemClickListener mOnMenuItemClickListener;
/** * 菜单状态 */
public enum Status {
OPEN, CLOSE
}
/** * 菜单的位置枚举类 */
public enum Position {
LEFT_TOP, LEFT_BOTTOM, RIGHT_TOP, RIGHT_BOTTOM
}
/** * 点击子菜单项的回调接口 */
public interface OnMenuItemClickListener {
void onClick(View view, int position);
}
public void setOnMenuItemClickListener(OnMenuItemClickListener mOnMenuItemClickListener) {
this.mOnMenuItemClickListener = mOnMenuItemClickListener;
}
public MyMenu(Context context) {
this(context, null);
}
public MyMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//菜单间隔的默认值,转换为标准尺寸
mInterval = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics());
Log.i("test", "mInterval = " + mInterval);
//获取自定义属性
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyMenu, defStyleAttr, 0);
int position = a.getInt(R.styleable.MyMenu_position, POS_RIGHT_BOTTOM);
switch (position) {
case POS_LEFT_TOP:
mPosition = Position.LEFT_TOP;
break;
case POS_LEFT_BOTTOM:
mPosition = Position.LEFT_BOTTOM;
break;
case POS_RIGHT_TOP:
mPosition = Position.RIGHT_TOP;
break;
case POS_RIGHT_BOTTOM:
mPosition = Position.RIGHT_BOTTOM;
break;
}
mInterval = (int) a.getDimension(R.styleable.MyMenu_interval, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics()));
Log.i("test", "mInterval = " + mInterval);
a.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
//测量child
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
layoutMainButton();
int count = getChildCount();
for (int i = 0; i < count - 1; i++) {
View child = getChildAt(i + 1);//getChildAt(0)是主按钮
child.setVisibility(View.GONE);
child.setTag("Item"+(i+1));
//子菜单顶点坐标间隔
int cl = l;
int ct = mInterval * (i + 1);
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;
}
child.layout(cl, ct, cl + cwidth, ct + cheight);
}
}
}
//主按钮定位函数
private void layoutMainButton() {
mMainButton = getChildAt(0);//获取主按钮
mMainButton.setOnClickListener(this);
int l = 0;//left
int t = 0;//top
int width = mMainButton.getMeasuredWidth();
int height = mMainButton.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;
}
mMainButton.layout(l, t, l + width, t + height);
}
//菜单触发事件
private void toggleMenu(int duration) {
int count = getChildCount();
for (int i = 0; i < count - 1; i++) {
final View childView = getChildAt(i + 1);
childView.setVisibility(View.VISIBLE);
//子菜单结束位置是0,按钮已经在那里了,只需要计算开始的位置
int ct = mInterval * (i + 1);
int yflag = -1;//菜单展开的模式,向上或者向下
if (mPosition == Position.LEFT_BOTTOM || mPosition == Position.RIGHT_BOTTOM) {
yflag = 1;
}
Animation tranAnim = null;
if (mCurrentStatus == Status.OPEN) {
tranAnim = new TranslateAnimation(0, 0, 0, ct * yflag);
childView.setClickable(false);
childView.setFocusable(false);
} else {
tranAnim = new TranslateAnimation(0, 0, ct * yflag, 0);
childView.setClickable(true);
childView.setFocusable(true);
}
tranAnim.setFillAfter(true);
tranAnim.setDuration(duration);
tranAnim.setStartOffset((i * 100) / count);//设置延时
childView.startAnimation(tranAnim);
//动画监听器,动画结束时按钮消失
tranAnim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (mCurrentStatus == Status.CLOSE) {
//参考http://www.cnblogs.com/albert1017/p/4724435.html
childView.clearAnimation();
childView.setVisibility(View.GONE);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
final int position = i;
childView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnMenuItemClickListener!=null) {
mOnMenuItemClickListener.onClick(childView, position);//回调方法
}
menuItemAnim(position);//点击子菜单的动画效果
changeStatus();
}
});
}
findViewById(R.id.id_mymenu).setAnimation(new AlphaAnimation(0.0f, 1.0f));
changeStatus();
}
//子按钮点击事件
private void menuItemAnim(int position) {
for (int i=0;i<getChildCount()-1;i++) {
View child = getChildAt(i+1);
if (i == position) {
child.startAnimation(scaleBigAnim(300));
// Toast.makeText(getContext(), child.getTag() + "被点击", Toast.LENGTH_SHORT).show();
} else {
child.startAnimation(scaleSmallAnim(300));
}
child.setClickable(false);
child.setFocusable(false);
}
}
//放大动画
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(1.0f,0.0f);
animationSet.addAnimation(scaleAnim);
animationSet.addAnimation(alphaAnim);
animationSet.setDuration(duration);
animationSet.setFillAfter(true);
return animationSet;
}
//缩小动画
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(1.0f,0.0f);
animationSet.addAnimation(scaleAnim);
animationSet.addAnimation(alphaAnim);
animationSet.setDuration(duration);
animationSet.setFillAfter(true);
return animationSet;
}
//改变菜单状态
private void changeStatus() {
mCurrentStatus = (mCurrentStatus == Status.OPEN ? Status.CLOSE : Status.OPEN);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.id_mainButton:
toggleMenu(300);//菜单触发,参数为菜单展开/关闭的时间
break;
}
}
}
在MainActivity中调用自定义控件就和使用别的控件一样,在此之前我们先把MyMenu的布局提取出来,单独放到一个xml文件里。
将MyMenu设置成在右下角,从activity_main中将MyMenu的布局代码剪切,放到该文件中,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<com.phoenix.myapplication.MyMenu android:id="@+id/id_mymenu" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" app:interval="80dp" app:position="right_bottom">
<ImageView android:id="@+id/id_mainButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_add_circle_black_48dp"/>
<LinearLayout android:id="@+id/id_item1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="item1">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="item1"/>
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_archive_grey600_48dp"/>
</LinearLayout>
<LinearLayout android:id="@+id/id_item2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="item2">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="item2"/>
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_backspace_grey600_48dp"/>
</LinearLayout>
<LinearLayout android:id="@+id/id_item3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="item3">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="item3"/>
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_block_grey600_48dp"/>
</LinearLayout>
<LinearLayout android:id="@+id/id_item4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="item4">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="item4"/>
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ic_content_copy_grey600_48dp"/>
</LinearLayout>
</com.phoenix.myapplication.MyMenu>
将MyMenu移除后,activit_main代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/main_background">
<include layout="@layout/mymenu_right_bottom_layout"/>
</RelativeLayout>
代码如下所示
public class MainActivity extends AppCompatActivity {
private MyMenu mMyMenu;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMyMenu = (MyMenu) findViewById(R.id.id_mymenu);
mMyMenu.setOnMenuItemClickListener(new MyMenu.OnMenuItemClickListener() {
@Override
public void onClick(View view, int position) {
Toast.makeText(MainActivity.this,view.getTag()+"被点击",Toast.LENGTH_SHORT).show();
}
});
}
}
至此,自定义控件即可完成。
对于Android的回调接口需要加深领悟,在自定义控件中不用实现具体的方法,只需要定义回调接口即可。具体实现可以在使用的时候,通过调用回调接口来实现,增加了自定义控件的复用性。
源代码下载Android仿印象笔记的自定义菜单控件