博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android:一步步开发一个高度可定制化的扩展菜单
阅读量:6573 次
发布时间:2019-06-24

本文共 11425 字,大约阅读时间需要 38 分钟。

效果图

本文想试着从头开始讲解,中间贴的代码只是部分的,如果需要全部代码请翻到最后,有造好的轮子和源码.

需求:

如效果图所示的效果大家应该见过很多了,但是很多都是把每个菜单的按钮的样式基本上固定了,虽然可以用但是对于不同的项目来说风格真的能搭配上吗?能不能做到每个菜单样式都能自己定义而且不用太过于麻烦?

实现思路:

1.自定义ViewGroup,用户只需要往这个组件里面添加按钮即可,组件负责处理菜单按钮的功能,显示,动画等等

2.添加菜单项要是能从xml文件中添加就更好了,方便预览菜单按钮的效果

详细思路:

之前了解过其他类似的项目的内部实现方式,有的是默认把所有按钮叠加在一起 ,让展开按钮覆盖后面的菜单按钮,点击展开按钮的时候用ObjectAnimation将其他组件移动到位置,个人觉得这样实现起来是否太过于复杂,为何不能先把菜单按钮放置到展开之后的位置,然后通过动画来做位移的效果,配合按钮的显示与隐藏也能达到同样的效果,而且菜单项本身是没有发生位置变化的.

代码实现:

1. 第一步,自定义一个ViewGroup,能够让添加到其中的View按照效果图摆放

设计思路:第一个ChildView和最后一个ChildView分别当作点击展开和关闭的按钮,都放置到右下角,其他的菜单按照自动换行的效果摆放,如下图

代码实现:

创建自定义ViewGroup,继承ViewGroup,重写构造方法:

public class ExpandableMenu extends ViewGroup {        public ExpandableMenu(@NonNull Context context) {        this(context, null);    }    public ExpandableMenu(@NonNull Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        mContext = context;    }复制代码

这时候需要重写onMeasure方法来测量子组件并且规定父组件的大小

循环测量子组件,因为一般菜单的使用场景就是覆盖到顶部,所以父组件的大小就干脆都设置为match_parent

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        for (int i = 0; i < getChildCount(); i++) {            View childView = getChildAt(i);            measureChild(childView, widthMeasureSpec, heightMeasureSpec);        }        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);    }复制代码

初次之外还需要重写onLayout方法来设置子View的位置

传入的参数依次为isChanged,left,top,right,bottom,其中的int值为父组件的四个方向的位置,就是我们用于放置子组件的依据

@Overrideprotected void onLayout(boolean b, int i, int i1, int i2, int i3) {}复制代码

规定子View的位置的方法为:

childView.layout(left, top, right, bottom);复制代码
能获取到childView的宽高,那么只需要确定其left,top的值即可获取到位置的四个参数了

如何获取子Viewleft,top?

简要的画了个图说明一下,原本是应该考虑每个组件的magin的,后来发现可以按照简单的方法来,不考虑magin,这样onLayout的实现会简单很多,而且可以用padding来达到和magin同样的效果

为了达到自动换行的效果,需要设置一个X方向和Y方向的标志位,每放置一个子View需要对X,Y的值进行变化,X的值是每次减少一个子View的宽度,Y不变,直到X<=0,即需要换行,此时X恢复,Y需要变化,具体的实现代码如下:

@Override    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {        int left = i2;        int top = i3;        int x = 0;        int y = 0;        for (int j = 0; j < getChildCount(); j++) {            View childView = getChildAt(j);            int width = childView.getMeasuredWidth();            int height = childView.getMeasuredHeight();            left = left - width;            x++;            if (top == i3) {                top = i3 -  height;            }            if (left < 0) {                y++;                x = 1;                left = i2 -  width;                top = top -  height;            }            if (j == getChildCount() - 1) {                // 最后一个,放置在第一个的位置                childView.layout(i2 - width, i3 - height, i2, i3);            } else {                childView.layout(left, top, left + width, top + height);            }        }    }复制代码

之后需要编写展开菜单和隐藏菜单的动画,这部分很简单,所以直接放代码,封装了两个方法,处理动画和菜单项的显示隐藏

其中位移动画是从第一个View的位置移动到当前的位置,位移的距离及是当前Viewleft,top值与第一个子View的对应参数的差.
/**     * 展开菜单     */    private void expand() {        // 隐藏第一个按钮        getChildAt(0).setVisibility(GONE);        // 显示最后一个按钮        getChildAt(getChildCount() - 1).setVisibility(VISIBLE);        isExpend = true;        for (int i = 1; i < getChildCount() - 1; i++) {            View childView = getChildAt(i);            TranslateAnimation animation = new TranslateAnimation(                    getChildAt(0).getLeft() - childView.getLeft(), 0.0f, getChildAt(0).getTop() - childView.getTop(), 0.0f            );            animation.setInterpolator(mInterpolator);            childView.setVisibility(VISIBLE);            animation.setDuration(mDuration);            childView.startAnimation(animation);        }    }复制代码

对应的隐藏菜单的方法

执行与展开相反的动画,并且在动画结束的时候把菜单项隐藏
/**     * 关闭菜单     */    private void close() {        // 显示第一个按钮        getChildAt(0).setVisibility(VISIBLE);        // 隐藏最后一个按钮        getChildAt(getChildCount() - 1).setVisibility(GONE);        // 收回菜单        isExpend = false;        for (int i = 1; i < getChildCount() - 1; i++) {            final View childView = getChildAt(i);            TranslateAnimation animation = new TranslateAnimation(                    0.0f, getChildAt(0).getLeft() - childView.getLeft(), 0.0f, getChildAt(0).getTop() - childView.getTop()            );            animation.setInterpolator(mInterpolator);            animation.setAnimationListener(new Animation.AnimationListener() {                @Override                public void onAnimationStart(Animation animation) {                }                @Override                public void onAnimationEnd(Animation animation) {                    childView.setVisibility(GONE);                }                @Override                public void onAnimationRepeat(Animation animation) {                }            });            childView.setVisibility(VISIBLE);            animation.setDuration(mDuration);            childView.startAnimation(animation);        }    }复制代码

之后需要给按钮设置展开和关闭的点击事件,同时默认隐藏其他按钮,代码如下:

private void init() {        for (int i = 0; i < getChildCount(); i++) {            View childView = getChildAt(i);            if (i != 0) {                childView.setVisibility(GONE);            }        }        // 设置点击事件        getChildAt(0).setOnClickListener(new OnClickListener() {            @Override            public void onClick(View view) {                expand();            }        });        getChildAt(getChildCount() - 1).setOnClickListener(new OnClickListener() {            @Override            public void onClick(View view) {                close();            }        });    }复制代码

init()方法需要在子View已经添加进来之后再调用,所以我将init方法放置在了onLayout之后,保证获取到的子View不会为空,但是onLayout在被调用很多次,所以加了个标志位,如下:

if (!isInited) {     init();     isInited = true;}复制代码

至此组件的编写就完了,没有多余的方法,要添加菜单可以直接在xml内添加,也可以用代码添加,只要注意菜单项的大小应当相同,并且第一个和最后一个view是用于展开和关闭的,至于点击事件,在添加到View之前设置即可,不用给第一个和最后一个view设置点击事件,因为设置了也会被覆盖掉.

xml使用方式如下:

复制代码

然后是整个ExpandableMenu的代码

package com.brioal.view;import android.content.Context;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;import android.view.animation.AccelerateDecelerateInterpolator;import android.view.animation.Animation;import android.view.animation.Interpolator;import android.view.animation.TranslateAnimation;/** * email:brioal@foxmail.com * github:https://github.com/Brioal * Created by Brioal on 2018/3/27. */public class ExpandableMenu extends ViewGroup {    private Context mContext;    // 动画的间隔    private int mDuration = 500;    // 插补器    private Interpolator mInterpolator = new AccelerateDecelerateInterpolator();    // 菜单是否展开了    private boolean isExpend = false;    // 是否已经初始化了    private boolean isInited = false;    public ExpandableMenu(@NonNull Context context) {        this(context, null);    }    public ExpandableMenu(@NonNull Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        mContext = context;    }    /**     * 设置动画间隔     *     * @param duration     */    public ExpandableMenu setDuration(int duration) {        mDuration = duration;        return this;    }    /**     * 设置插补器     *     * @param interpolator     */    public ExpandableMenu setInterpolator(Interpolator interpolator) {        mInterpolator = interpolator;        return this;    }    @Override    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {        int left = i2;        int top = i3;        int x = 0;        int y = 0;        for (int j = 0; j < getChildCount(); j++) {            View childView = getChildAt(j);            int width = childView.getMeasuredWidth();            int height = childView.getMeasuredHeight();            left = left - width;            x++;            if (top == i3) {                top = i3 -  height;            }            if (left < 0) {                y++;                x = 1;                left = i2 -  width;                top = top -  height;            }            if (j == getChildCount() - 1) {                // 最后一个,放置在第一个的位置                childView.layout(i2 - width, i3 - height, i2, i3);            } else {                childView.layout(left, top, left + width, top + height);            }        }        if (!isInited) {            init();            isInited = true;        }    }    private void init() {        for (int i = 0; i < getChildCount(); i++) {            View childView = getChildAt(i);            if (i != 0) {                childView.setVisibility(GONE);            }        }        // 设置点击事件        getChildAt(0).setOnClickListener(new OnClickListener() {            @Override            public void onClick(View view) {                expand();            }        });        getChildAt(getChildCount() - 1).setOnClickListener(new OnClickListener() {            @Override            public void onClick(View view) {                close();            }        });    }    /**     * 菜单是否是展开的     * @return     */    public boolean isExpend() {        return isExpend;    }    /**     * 关闭菜单     */    private void close() {        // 显示第一个按钮        getChildAt(0).setVisibility(VISIBLE);        // 隐藏最后一个按钮        getChildAt(getChildCount() - 1).setVisibility(GONE);        // 收回菜单        isExpend = false;        for (int i = 1; i < getChildCount() - 1; i++) {            final View childView = getChildAt(i);            TranslateAnimation animation = new TranslateAnimation(                    0.0f, getChildAt(0).getLeft() - childView.getLeft(), 0.0f, getChildAt(0).getTop() - childView.getTop()            );            animation.setInterpolator(mInterpolator);            animation.setAnimationListener(new Animation.AnimationListener() {                @Override                public void onAnimationStart(Animation animation) {                }                @Override                public void onAnimationEnd(Animation animation) {                    childView.setVisibility(GONE);                }                @Override                public void onAnimationRepeat(Animation animation) {                }            });            childView.setVisibility(VISIBLE);            animation.setDuration(mDuration);            childView.startAnimation(animation);        }    }    /**     * 展开菜单     */    private void expand() {        // 隐藏第一个按钮        getChildAt(0).setVisibility(GONE);        // 显示最后一个按钮        getChildAt(getChildCount() - 1).setVisibility(VISIBLE);        isExpend = true;        for (int i = 1; i < getChildCount() - 1; i++) {            View childView = getChildAt(i);            TranslateAnimation animation = new TranslateAnimation(                    getChildAt(0).getLeft() - childView.getLeft(), 0.0f, getChildAt(0).getTop() - childView.getTop(), 0.0f            );            animation.setInterpolator(mInterpolator);            childView.setVisibility(VISIBLE);            animation.setDuration(mDuration);            childView.startAnimation(animation);        }    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        for (int i = 0; i < getChildCount(); i++) {            View childView = getChildAt(i);            measureChild(childView, widthMeasureSpec, heightMeasureSpec);        }        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);    }}复制代码

轮子地址:

转载地址:http://crmjo.baihongyu.com/

你可能感兴趣的文章
(小蚂蚁站长吧)网站优化做好这八步你就是seo第一
查看>>
使用流的方式往页面前台输出图片
查看>>
java核心技术反射
查看>>
我的友情链接
查看>>
Maven创建新的依赖项目
查看>>
2015年10月26日作业
查看>>
LAMP,安装脚本
查看>>
面向对象题目
查看>>
Java异常总结
查看>>
DHCP
查看>>
电脑上怎样压缩图片大小
查看>>
新来的发一个帖子
查看>>
Nginx 支持webSocket 响应403
查看>>
lnmp安装
查看>>
3.两种密钥配对方法,很简单哦《Mr.Robot》
查看>>
FTP工作方式
查看>>
Linux之安装部署squid代理服务器
查看>>
Linux文件和目录管理常用命令(中)
查看>>
Configure HUE to store data in MySQL
查看>>
我的友情链接
查看>>