效果图
本文想试着从头开始讲解,中间贴的代码只是部分的,如果需要全部代码请翻到最后,有造好的轮子和源码.
需求:
如效果图所示的效果大家应该见过很多了,但是很多都是把每个菜单的按钮的样式基本上固定了,虽然可以用但是对于不同的项目来说风格真的能搭配上吗?能不能做到每个菜单样式都能自己定义而且不用太过于麻烦?
实现思路:
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
值为父组件的四个方向的位置,就是我们用于放置子组件的依据
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
}
规定子View的位置的方法为:
childView.layout(left, top, right, bottom);
能获取到childView
的宽高,那么只需要确定其left
,top
的值即可获取到位置的四个参数了
如何获取子View
的left
,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
的位置移动到当前的位置,位移的距离及是当前View
的left
,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
不会为空,但是onLayou
t在被调用很多次,所以加了个标志位,如下:
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