背景
最近的项目中用到了类似美团中的下拉多选菜单,在实际开发过程中,也发现了一些问题,主要归纳如下:
1.当菜单较为复杂时,如果不能设计好代码逻辑,将造成控件难于维护
2.美团菜单可以连续点击顶部tab,切换不同菜单,而我使用的popupWindow似乎在展开一个菜单时点击其他tab,菜单就会收回。
本文将针对如上两个问题进行一些讨论,最终给出较为合理的解决方案。
程序结构
由于菜单涉及多级多项,如果把UI和其他逻辑堆在一起写,必然会造成代码过于庞大,甚至没有办法扩展,更谈不上及时变更需求。
ViewHolder与组合控件结合分割菜单逻辑
这里我采用了组合控件和ViewHolder结合的办法来处理耦合的问题。
组合控件的特点是可以直接定义在xml里无需做其他任何多余的操作,ViewHolder则可以灵活地提供View,并将这些View贴到需要的地方。
基于上述特征,我将固定的菜单栏设计为组合控件,提供各项菜单的tab,而将菜单的具体内容使用ViewHolder封装,在需要的时候从ViewHoder中拿到View,贴到我们需要放置的地方。同时,每个菜单中的UI逻辑也会被封装到ViewHolder中,这样,如果我们需要修改需求,直接改动对应的ViewHolder的代码,而不会影响其他代码。
这样我们代码就可以将复杂的UI逻辑分成相互独立的小块,想改哪里改哪里,妈妈再也不用担心产品经理为难我了…………
使用布局文件代替popupWindow
翻阅网上很多仿制的美团菜单例程,几乎都没有真正和美团app的菜单一样,我们可以查看官方app,点击一个tab展开菜单,当在点击下一个tab时,菜单并没有收回,而是显示了当前tab对应的内容。
由于很多demo都是使用popupWindow作为菜单的载体,而我在实际操作过程中发现popupWindow作为模态对话框非常难控制,而且还会引起其他问题,总之,我认为此处使用使用popupWindow并不合适。我在给菜单栏下面放了一块空布局,当向空布局中添加View时,空布局扩大,也就形成了下拉菜单的效果。
那么有同学要问了,这样的话不就会影响下面的其他布局的位置了?是的,所以我们的菜单栏必须放在RelativeLayout或者FrameLayout这类结构中,而且必须放在其顶层。
控件原型
了解了上述两个问题的解决方法,我们就可以大概勾勒一样我们的控件大体的模样了。如下图:
点击TAB1,TAB2,TAB3,内容View被对应的Holder中维护的View替换,我们清空内容View中的view时,由于这个View是包裹内容的,内容为空时高度自然变成0,也就是菜单收起的状态。我们可以为每个Holder设置相应的回调接口,这样我们的菜单View就能根据Holder的变化实时做出响应。
代码实现
1.ViewHolder
ViewHolder用来维护一个我们手动inflate出来的View,并提供刷新数据的方法。我们可以以此为基类,封装UI逻辑,ViewHolder间也可以灵活替换。
/**
* 自绘控件封装类
* Created by vonchenchen on 2015/11/3 0003.
*/
public abstract class BaseWidgetHolder<T> {
protected View mRootView;
protected Context mContext;
public abstract View initView();
public abstract void refreshView(T data);
public BaseWidgetHolder(Context context){
mContext = context;
mRootView = initView();
mRootView.setTag(this);
}
public View getRootView(){
return mRootView;
}
}
2.菜单View
这个View就是菜单栏主View,包括了三个TAB和下面的内容View,我们只需在工程中直接将这个类放入我们的布局文件中就可以了,注意,必须放在RelativieLayout或者FrameLayout中,而且必须是最顶层,否则内容View展开时会“挤”到其他布局。此处我们采用这种方式而不是popupWindow是因为popupWindow焦点改变可能会触发消失,这样无法实现点击不同的tab时,连续切换菜单的效果。
/**
*
* 搜索菜单栏
* Created by vonchenchen on 2016/4/5 0005.
*/
public class SelectMenuView extends LinearLayout{
private static final int TAB_SUBJECT = 1;
private static final int TAB_SORT = 2;
private static final int TAB_SELECT = 3;
private Context mContext;
private View mSubjectView;
private View mSortView;
private View mSelectView;
private View mRootView;
private View mPopupWindowView;
private RelativeLayout mMainContentLayout;
private View mBackView;
/** type1 */
private SubjectHolder mSubjectHolder;
/** type2 */
private SortHolder mSortHolder;
/** type3 */
private SelectHolder mSelectHolder;
/** 与外部通信传递数据的接口 */
private OnMenuSelectDataChangedListener mOnMenuSelectDataChangedListener;
private RelativeLayout mContentLayout;
private TextView mSubjectText;
private ImageView mSubjectArrowImage;
private TextView mSortText;
private ImageView mSortArrowImage;
private TextView mSelectText;
private ImageView mSelectArrowImage;
private List<String> mGroupList;
private List<String> mPrimaryList;
private List<String> mJuniorList;
private List<String> mHighList;
private List<List<String>> mSubjectDataList;
private int mTabRecorder = -1;
public SelectMenuView(Context context) {
super(context);
this.mContext = context;
this.mRootView = this;
init();
}
public SelectMenuView(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
this.mRootView = this;
init();
}
private void init(){
mGroupList = new ArrayList<String>();
mGroupList.add("A");
mGroupList.add("B");
mGroupList.add("C");
mPrimaryList = new ArrayList<String>();
mPrimaryList.add("A1");
mPrimaryList.add("A2");
mPrimaryList.add("A3");
mJuniorList = new ArrayList<String>();
mJuniorList.add("B1");
mJuniorList.add("B2");
mJuniorList.add("B3");
mJuniorList.add("B4");
mJuniorList.add("B5");
mJuniorList.add("B6");
mJuniorList.add("B7");
mJuniorList.add("B8");
mJuniorList.add("B9");
mHighList = new ArrayList<String>();
mHighList.add("C1");
mHighList.add("C2");
mHighList.add("C3");
mHighList.add("C4");
mHighList.add("C5");
mHighList.add("C6");
mHighList.add("C7");
mHighList.add("C8");
mHighList.add("C9");
mSubjectDataList = new ArrayList<List<String>>();
mSubjectDataList.add(mGroupList);
mSubjectDataList.add(mPrimaryList);
mSubjectDataList.add(mJuniorList);
mSubjectDataList.add(mHighList);
//type1
mSubjectHolder = new SubjectHolder(mContext);
mSubjectHolder.refreshData(mSubjectDataList, 0, -1);
mSubjectHolder.setOnRightListViewItemSelectedListener(new SubjectHolder.OnRightListViewItemSelectedListener() {
@Override
public void OnRightListViewItemSelected(int leftIndex, int rightIndex, String text) {
if(mOnMenuSelectDataChangedListener != null){
int grade = leftIndex+1;
int subject = getSubjectId(rightIndex);
mOnMenuSelectDataChangedListener.onSubjectChanged(grade+"", subject+"");
}
dismissPopupWindow();
//Toast.makeText(UIUtils.getContext(), text, Toast.LENGTH_SHORT).show();
mSubjectText.setText(text);
}
});
//type2
mSortHolder = new SortHolder(mContext);
mSortHolder.setOnSortInfoSelectedListener(new SortHolder.OnSortInfoSelectedListener() {
@Override
public void onSortInfoSelected(String info) {
if(mOnMenuSelectDataChangedListener != null){
mOnMenuSelectDataChangedListener.onSortChanged(info);
}
dismissPopupWindow();
mSortText.setText(getSortString(info));
//Toast.makeText(UIUtils.getContext(), info, Toast.LENGTH_SHORT).show();
}
});
//type3
mSelectHolder = new SelectHolder(mContext);
mSelectHolder.setOnSelectedInfoListener(new SelectHolder.OnSelectedInfoListener() {
@Override
public void OnselectedInfo(String gender, String type) {
if(mOnMenuSelectDataChangedListener != null){
mOnMenuSelectDataChangedListener.onSelectedChanged(gender, type);
}
dismissPopupWindow();
//Toast.makeText(UIUtils.getContext(), gender+" "+type, Toast.LENGTH_SHORT).show();
}
});
}
private int getSubjectId(int index){
return index;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View.inflate(mContext, R.layout.layout_search_menu, this);
mSubjectText = (TextView) findViewById(R.id.subject);
mSubjectArrowImage = (ImageView) findViewById(R.id.img_sub);
mSortText = (TextView) findViewById(R.id.comprehensive_sorting);
mSortArrowImage = (ImageView) findViewById(R.id.img_cs);
mSelectText = (TextView) findViewById(R.id.tv_select);
mSelectArrowImage = (ImageView) findViewById(R.id.img_sc);
mContentLayout = (RelativeLayout) findViewById(R.id.rl_content);
mPopupWindowView = View.inflate(mContext, R.layout.layout_search_menu_content, null);
mMainContentLayout = (RelativeLayout) mPopupWindowView.findViewById(R.id.rl_main);
//mBackView = mPopupWindowView.findViewById(R.id.ll_background);
mSubjectView = findViewById(R.id.ll_subject);
mSortView = findViewById(R.id.ll_sort);
mSelectView = findViewById(R.id.ll_select);
//点击 type1 弹出菜单
mSubjectView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mOnMenuSelectDataChangedListener != null){
mOnMenuSelectDataChangedListener.onViewClicked(mSubjectView);
}
handleClickSubjectView();
}
});
//点击 type2 弹出菜单
mSortView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mOnMenuSelectDataChangedListener != null){
mOnMenuSelectDataChangedListener.onViewClicked(mSortView);
}
handleClickSortView();
}
});
//点击 type3 弹出菜单
mSelectView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mOnMenuSelectDataChangedListener != null){
mOnMenuSelectDataChangedListener.onViewClicked(mSelectView);
}
handleClickSelectView();
}
});
//点击黑色半透明部分,菜单收回
mContentLayout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dismissPopupWindow();
}
});
}
private void handleClickSubjectView(){
//清空内容View中的View
mMainContentLayout.removeAllViews();
//将我们已经创建好的ViewHolder拿出,取出其中的View贴到内容View中
mMainContentLayout.addView(mSubjectHolder.getRootView(), ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
//处理弹窗动作
popUpWindow(TAB_SUBJECT);
}
private void handleClickSortView(){
mMainContentLayout.removeAllViews();
mMainContentLayout.addView(mSortHolder.getRootView(), ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
popUpWindow(TAB_SORT);
}
private void handleClickSelectView(){
mMainContentLayout.removeAllViews();
mMainContentLayout.addView(mSelectHolder.getRootView(), ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
popUpWindow(TAB_SELECT);
}
private void popUpWindow(int tab){
if(mTabRecorder != -1) {
resetTabExtend(mTabRecorder);
}
extendsContent();
setTabExtend(tab);
mTabRecorder = tab;
}
private void extendsContent(){
mContentLayout.removeAllViews();
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mContentLayout.addView(mPopupWindowView, params);
}
private void dismissPopupWindow(){
mContentLayout.removeAllViews();
setTabClose();
}
public void setOnMenuSelectDataChangedListener(OnMenuSelectDataChangedListener onMenuSelectDataChangedListener){
this.mOnMenuSelectDataChangedListener = onMenuSelectDataChangedListener;
}
public interface OnMenuSelectDataChangedListener{
void onSubjectChanged(String grade, String subjects);
void onSortChanged(String sortType);
void onSelectedChanged(String gender, String classType);
void onViewClicked(View view);
//筛选菜单,当点击其他处菜单收回后,需要更新当前选中项
void onSelectedDismissed(String gender, String classType);
}
private void setTabExtend(int tab){
if(tab == TAB_SUBJECT){
mSubjectText.setTextColor(getResources().getColor(R.color.blue));
mSubjectArrowImage.setImageResource(R.mipmap.ic_up_blue);
}else if(tab == TAB_SORT){
mSortText.setTextColor(getResources().getColor(R.color.blue));
mSortArrowImage.setImageResource(R.mipmap.ic_up_blue);
}else if(tab == TAB_SELECT){
mSelectText.setTextColor(getResources().getColor(R.color.blue));
mSelectArrowImage.setImageResource(R.mipmap.ic_up_blue);
}
}
private void resetTabExtend(int tab){
if(tab == TAB_SUBJECT){
mSubjectText.setTextColor(getResources().getColor(R.color.gray));
mSubjectArrowImage.setImageResource(R.mipmap.ic_down);
}else if(tab == TAB_SORT){
mSortText.setTextColor(getResources().getColor(R.color.gray));
mSortArrowImage.setImageResource(R.mipmap.ic_down);
}else if(tab == TAB_SELECT){
mSelectText.setTextColor(getResources().getColor(R.color.gray));
mSelectArrowImage.setImageResource(R.mipmap.ic_down);
}
}
private void setTabClose(){
mSubjectText.setTextColor(getResources().getColor(R.color.text_color_gey));
mSubjectArrowImage.setImageResource(R.mipmap.ic_down);
mSortText.setTextColor(getResources().getColor(R.color.text_color_gey));
mSortArrowImage.setImageResource(R.mipmap.ic_down);
mSelectText.setTextColor(getResources().getColor(R.color.text_color_gey));
mSelectArrowImage.setImageResource(R.mipmap.ic_down);
}
private String getSortString(String info){
if(SortHolder.SORT_BY_NORULE.equals(info)){
return "sort1";
}else if(SortHolder.SORT_BY_EVALUATION.equals(info)){
return "sort2";
}else if(SortHolder.SORT_BY_PRICELOW.equals(info)){
return "sort3";
}else if(SortHolder.SORT_BY_PRICEHIGH.equals(info)){
return "sort4";
}else if(SortHolder.SORT_BY_DISTANCE.equals(info)){
return "sort5";
}
return "sort1";
}
public void clearAllInfo(){
//清除控件内部选项
mSubjectHolder.refreshData(mSubjectDataList, 0, -1);
mSortHolder.refreshView(null);
mSelectHolder.refreshView(null);
//清除菜单栏显示
mSubjectText.setText("type1");
mSortText.setText("type2");
}
}
以下是demo的实现效果,点击不同tab,下面菜单实现连续切换:
代码地址 https://git.oschina.net/vonchenchen/menu_demo.git
下载地址 http://download.csdn.net/detail/lidec/9498648