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的话,需要实现回调接口里的方法(也可以不实现)。

子菜单的点击动画,我们点击的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仿印象笔记的自定义菜单控件

更多相关文章

  1. Android(安卓)Preference使用
  2. Android控件_ProgressBar使用
  3. Android学习笔记の五
  4. Android四种常用布局
  5. RelativeLayout各个属性
  6. android 焦点问题
  7. 自定义Spinner五步走
  8. android 按钮设计中state_selected属性
  9. Your content must have a ListView whose id attribute is 'and

随机推荐

  1. android zbar使用
  2. android文件打印--printerShare
  3. Android(安卓)解决Activity切换时出现白
  4. Android Audio Policy小记
  5. eclipse 配置已安装Android SDK 快速配置
  6. android Google推荐的容器SparseArrayCom
  7. Android编程获取网络连接状态(3G/Wifi)及
  8. Android个人手机通讯录开发详解
  9. Android(安卓)Launcher3浅析(一)
  10. ionic3文件目录介绍