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

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