前言:
首先来看看我们要实现的效果,下面这张图是大众点评APP里面的一个多条目下拉菜单筛选的一个效果,这是很多App里面都比较常见的一种多条目筛选菜单。
效果图:
![](https://i-blog.csdnimg.cn/blog_migrate/19c8855539ef86de32b47251f521424f.gif)
结构分析:
![](https://i-blog.csdnimg.cn/blog_migrate/f52890956bff2c226e0ffbf3fa30b575.jpeg)
整个控件我么用一个LinearLayout 实现,所以我们要继承LinearLayout .然后是上面红色长方形部分的Tab栏,下面最外层黑色框框是一个FrameLayout用来存放阴影(绿色框框部分)和菜单布局(最里面红色框框部分)
代码实现:
分析完了,我们就可以用代码来实现了,代码如下:
1、基本布局:
public class ListDataScreenView extends LinearLayout {
private Context mContext;
// 1.1 创建头部用来存放 Tab
private LinearLayout mMenuTabView;
// 1.2 创建 FrameLayout 用来存放 = 阴影(View) + 菜单内容布局(FrameLayout)
private FrameLayout mMenuMiddleView;
// 阴影
private View mShadowView;
// 创建菜单用来存放菜单内容
private FrameLayout mMenuContainerView;
// 阴影的颜色
private int mShadowColor = 0x88888888;
// 筛选菜单的 Adapter
private BaseMenuAdapter mAdapter;
// 内容菜单的高度
private int mMenuContainerHeight;
// 当前打开的位置
private int mCurrentPosition = -1;
private long DURATION_TIME = 350;
// 动画是否在执行
private boolean mAnimatorExecute;
public ListDataScreenView(Context context) {
this(context, null);
}
public ListDataScreenView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ListDataScreenView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initLayout();
}
/**
* 1.布局实例化好 (组合控件)
*/
private void initLayout() {
// 1. 先创建一个 xml 布局 ,再加载,findViewById
// 2. 简单的效果用代码去创建 早期IOS 用代码创建布局
setOrientation(VERTICAL);
// 1.1 创建头部用来存放 Tab
mMenuTabView = new LinearLayout(mContext);
mMenuTabView.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
addView(mMenuTabView);
// 1.2 创建 FrameLayout 用来存放 = 阴影(View) + 菜单内容布局(FrameLayout)
mMenuMiddleView = new FrameLayout(mContext);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0);
params.weight = 1;
mMenuMiddleView.setLayoutParams(params);
addView(mMenuMiddleView);
// 创建阴影 可以不用设置 LayoutParams 默认就是 MATCH_PARENT ,MATCH_PARENT
mShadowView = new View(mContext);
mShadowView.setBackgroundColor(mShadowColor);
mShadowView.setAlpha(0f);
mShadowView.setOnClickListener(this);
mShadowView.setVisibility(GONE);
mMenuMiddleView.addView(mShadowView);//阴影View在下面
// 创建菜单用来存放菜单内容
mMenuContainerView = new FrameLayout(mContext);
mMenuContainerView.setBackgroundColor(Color.WHITE);
mMenuMiddleView.addView(mMenuContainerView);//菜单内容在上面
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e("TAG", "onMeasure");
int height = MeasureSpec.getSize(heightMeasureSpec);
if (mMenuContainerHeight == 0 && height > 0) {
// 内容的高度应该不是全部 应该是整个 View的 75%
mMenuContainerHeight = (int) (height * 75f / 100);
//获取菜单内容View的LayoutParams
ViewGroup.LayoutParams params = mMenuContainerView.getLayoutParams();
//设置菜单内容的高度
params.height = mMenuContainerHeight;
mMenuContainerView.setLayoutParams(params);
// 进来的时候阴影不显示 ,内容也是不显示的(把它移上去)
//放菜单内容
mMenuContainerView.setTranslationY(-mMenuContainerHeight);
}
}
}
2、设置点击事件,以及过程中的动画
/**
* 设置tab的点击
*
* @param tabView
* @param position
*/
private void setTabClick(final View tabView, final int position) {
tabView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mCurrentPosition == -1) {
// 没打开
openMenu(position, tabView);
} else {
if (mCurrentPosition == position) {
// 打开了,关闭
closeMenu();
} else {
// 切换一下显示
//拿到菜单容器里的子view(TextView)
View currentMenu = mMenuContainerView.getChildAt(mCurrentPosition);
currentMenu.setVisibility(View.GONE);//将子view(TextView)设置无不可见
mAdapter.menuClose(mMenuTabView.getChildAt(mCurrentPosition));
//拿到当前点击的位置
mCurrentPosition = position;
currentMenu = mMenuContainerView.getChildAt(mCurrentPosition);
currentMenu.setVisibility(View.VISIBLE);
mAdapter.menuOpen(mMenuTabView.getChildAt(mCurrentPosition));
}
}
}
});
}
/**
* 关闭菜单
*/
private void closeMenu() {
//动画正在执行,点击无效
if (mAnimatorExecute) {
return;
}
// 关闭动画 位移动画 透明度动画
ObjectAnimator translationAnimator = ObjectAnimator.ofFloat(mMenuContainerView, "translationY", 0, -mMenuContainerHeight);
translationAnimator.setDuration(DURATION_TIME);
translationAnimator.start();
mShadowView.setVisibility(View.VISIBLE);
ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(mShadowView, "alpha", 1f, 0f);
alphaAnimator.setDuration(DURATION_TIME);
// 要等关闭动画执行完才能去隐藏当前菜单
alphaAnimator.addListener(new AnimatorListenerAdapter() {
//动画执行完毕
@Override
public void onAnimationEnd(Animator animation) {
View menuView = mMenuContainerView.getChildAt(mCurrentPosition);
menuView.setVisibility(View.GONE);
mCurrentPosition = -1;
mShadowView.setVisibility(GONE);
mAnimatorExecute = false;
}
//关闭菜单动画开始
@Override
public void onAnimationStart(Animator animation) {
mAnimatorExecute = true;
mAdapter.menuClose(mMenuTabView.getChildAt(mCurrentPosition));
}
});
alphaAnimator.start();
}
/**
* 打开菜单
*
* @param position
* @param tabView
*/
private void openMenu(final int position, final View tabView) {
//动画正在执行,点击无效
if (mAnimatorExecute) {
return;
}
//设置阴影可见
mShadowView.setVisibility(View.VISIBLE);
// 获取当前位置显示当前菜单,菜单是加到了菜单容器
View menuView = mMenuContainerView.getChildAt(position);
menuView.setVisibility(View.VISIBLE);
// 打开开启动画 位移动画 透明度动画
//位移动画
ObjectAnimator translationAnimator = ObjectAnimator.ofFloat(mMenuContainerView, "translationY", -mMenuContainerHeight, 0);
translationAnimator.setDuration(DURATION_TIME);
translationAnimator.start();
//透明度动画
ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(mShadowView, "alpha", 0f, 1f);
alphaAnimator.setDuration(DURATION_TIME);
alphaAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mAnimatorExecute = false;
mCurrentPosition = position;
}
@Override
public void onAnimationStart(Animator animation) {
mAnimatorExecute = true;
// 把当前的 tab 传到外面
mAdapter.menuOpen(tabView);
}
});
alphaAnimator.start();
}
3、写Adapter提供数据源
这个时候我们的效果还是死的,我么得给他设置写一个Adapter,现实情况下,我们的数据也是通常要去网络获取,所以不能写死。
/**
* 筛选菜单的 Adapter
*/
public abstract class BaseMenuAdapter {
// 获取总共有多少条
public abstract int getCount();
// 获取当前的TabView
public abstract View getTabView(int position, ViewGroup parent);
// 获取当前的菜单内容
public abstract View getMenuView(int position, ViewGroup parent);
/**
* 菜单打开
* @param tabView
*/
public void menuOpen(View tabView) {
}
/**
* 菜单关闭
* @param tabView
*/
public void menuClose(View tabView) {
}
}
具体的Adapter:
public class ListScreenMenuAdapter extends BaseMenuAdapter{
private Context mContext;
public ListScreenMenuAdapter(Context context){
this.mContext = context;
}
private String[] mItems ={"附近","美食","智能排序","筛选"};
@Override
public int getCount() {
return mItems.length;
}
@Override
public View getTabView(int position, ViewGroup parent) {
TextView tabView = (TextView) LayoutInflater.from(mContext).inflate(R.layout.ui_list_data_screen_tab,parent,false);
tabView.setText(mItems[position]);
tabView.setTextColor(Color.BLACK);
return tabView;
}
@Override
public View getMenuView(int position, ViewGroup parent) {
// 真正开发过程中,不同的位置显示的布局不一样
TextView menuView = (TextView) LayoutInflater.from(mContext).inflate(R.layout.ui_list_data_screen_menu,parent,false);
menuView.setText(mItems[position]);
return menuView;
}
@Override
public void menuClose(View tabView) {
TextView tabTv = (TextView) tabView;
tabTv.setTextColor(Color.BLACK);
}
@Override
public void menuOpen(View tabView) {
TextView tabTv = (TextView) tabView;
tabTv.setTextColor(Color.RED);
}
}
有了Adapter之后,我们就可以给ListDataScreenView 以下方法,是不是感觉跟ListView很像。
/**
* 设置 Adapter
*
* @param adapter
*/
public void setAdapter(BaseMenuAdapter adapter) {
this.mAdapter = adapter;
// 获取有多少条
int count = mAdapter.getCount();
for (int i = 0; i < count; i++) {
// 获取菜单的Tab
View tabView = mAdapter.getTabView(i, mMenuTabView);
mMenuTabView.addView(tabView);//加一个TextView
LinearLayout.LayoutParams params = (LayoutParams) tabView.getLayoutParams();
params.weight = 1;
tabView.setLayoutParams(params);
// 设置tab点击事件
setTabClick(tabView, i);
// 获取菜单的内容(一个TextView)
View menuView = mAdapter.getMenuView(i, mMenuContainerView);
menuView.setVisibility(GONE);
mMenuContainerView.addView(menuView);
}
// 内容还没有显示出来,打开的时候显示当前位置的菜单,关闭的时候隐藏,阴影点击应该要关闭菜单
// 动画在执行的情况下就不要在响应动画事件
// 打开和关闭 变化tab的显示 , 肯定不能把代码写到 ListDataScreen 里面来
// 当菜单是打开的状态 不要执行动画只要切换
}
最后看下我们实现的效果:
![](https://i-blog.csdnimg.cn/blog_migrate/4626528f653b398aed06df6feaf6a3a8.gif)