Android仿印象笔记的自定义菜单控件
Android仿印象笔记的自定义菜单控件
- Android仿印象笔记的自定义菜单控件
- 导读
- 效果图
- 准备图片资源
- 自定义控件attr属性
- 在XML文件中使用自定义控件
- 编写MyMenu类
- MyMenu类的构造方法
- onMeasure方法
- onLayout方法
- toggleMenu方法
- menuItemAnim方法
- 在MainActivity中调用自定义菜单
- mymenu_right_bottom
- activity_main的代码如下
- MainActivity
- 后记
导读
今天在慕课网上看到一篇视频教程,Android实现卫星菜单,感觉效果非常炫酷,想到印象笔记添加笔记的菜单也可以通过这种方式来实现,点击添加笔记菜单按钮,便会弹出一系列的按钮用于添加不同的笔记。于是自己试着仿照印象笔记的菜单按钮,写出一个自定义的菜单控件。
效果图
先上一下效果图,看看完成后的效果如何
准备图片资源
我们先准备一下自定义菜单所需要的资源。Google发布了Material Design Icons,正好可以被我们拿来用,下载地址Material Design icon合集 ,选出我们需要的图标拷贝到Android Studio里。
自定义控件attr属性
首先,我们需要编写自定义控件的属性。在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文件中使用自定义控件
在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类
还记得上一节我们写的MyMenu类吗?现在我们来完善它。
MyMenu类的构造方法
需要设置每个菜单按钮的间隔,需要设置菜单的位置,这些都可以通过自定义属性的值获得。别忘了设置默认值,最后要recycle。
onMeasure方法
对每个子控件调用measureChild方法。
onLayout方法
在onLayout方法里,首先需要定位主按钮的位置。定义layoutMainButton方法,判断主按钮在屏幕的哪个角落。
之后,根据主按钮的位置计算子按钮的位置。如果主按钮在顶部,则子按钮的y轴坐标逐渐增加;如果主按钮在顶部,则子按钮的y轴坐标逐渐减少。
在这里可以设置子按钮的Tag,方便日后进行判断操作。
toggleMenu方法
主按钮点击事件,如果菜单处于打开状态,则关闭菜单;如果菜单处于关闭状态,则打开菜单。里面对子按钮增加了动画和动画监听器,在动画结束后设置子按钮是否可见。点击后,需要通过changeStatus方法改变菜单的状态。
在此方法里,通过定义的OnMenuItemClickListener接口,实现子按钮的点击事件。同时,如果回调接口不为0的话,需要实现回调接口里的方法(也可以不实现)。
menuItemAnim方法
子菜单的点击动画,我们点击的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中调用自定义菜单
在MainActivity中调用自定义控件就和使用别的控件一样,在此之前我们先把MyMenu的布局提取出来,单独放到一个xml文件里。
mymenu_right_bottom
将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>
activity_main的代码如下
将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>
MainActivity
代码如下所示
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仿印象笔记的自定义菜单控件
更多相关文章
- Android控件_ProgressBar使用
- 系出名门Android(7) - 控件(View)之ZoomControls, Include, Vide
- 布局与控件(三)-TextView那些事儿
- android EditText控件自动获取焦点弹出键盘解决方法