Android:一步步开发一个高度可定制化的扩展菜单

效果图

Android:一步步开发一个高度可定制化的扩展菜单_第1张图片
这里写图片描述

Android:一步步开发一个高度可定制化的扩展菜单_第2张图片
这里写图片描述

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

需求:

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

实现思路:

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

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

详细思路:

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

代码实现:

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

设计思路:第一个ChildView和最后一个ChildView分别当作点击展开和关闭的按钮,都放置到右下角,其他的菜单按照自动换行的效果摆放,如下图
Android:一步步开发一个高度可定制化的扩展菜单_第3张图片
这里写图片描述

代码实现:

创建自定义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值为父组件的四个方向的位置,就是我们用于放置子组件的依据
 @Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {

}
规定子View的位置的方法为:
childView.layout(left, top, right, bottom);
能获取到childView的宽高,那么只需要确定其left,top的值即可获取到位置的四个参数了

如何获取子Viewleft,top?

Android:一步步开发一个高度可定制化的扩展菜单_第4张图片
这里写图片描述
简要的画了个图说明一下,原本是应该考虑每个组件的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:[email protected]
 * 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);
    }

}

轮子地址:ExpandableMenu

你可能感兴趣的:(Android:一步步开发一个高度可定制化的扩展菜单)