Android自定义控件:仿美团下拉菜单及相关代码优化
qibaoan8
9年前
<h2>背景</h2> <p>最近的项目中用到了类似美团中的下拉多选菜单,在实际开发过程中,也发现了一些问题,主要归纳如下:</p> <pre> <code class="language-java">1.当菜单较为复杂时,如果不能设计好代码逻辑,将造成控件难于维护 2.美团菜单可以连续点击顶部tab,切换不同菜单,而我使用的popupWindow似乎在展开一个菜单时点击其他tab,菜单就会收回。 </code></pre> <p>本文将针对如上两个问题进行一些讨论,最终给出较为合理的解决方案。</p> <h2>程序结构</h2> <p>由于菜单涉及多级多项,如果把UI和其他逻辑堆在一起写,必然会造成代码过于庞大,甚至没有办法扩展,更谈不上及时变更需求。</p> <p>ViewHolder与组合控件结合分割菜单逻辑</p> <p>这里我采用了组合控件和ViewHolder结合的办法来处理耦合的问题。</p> <p>组合控件的特点是可以直接定义在xml里无需做其他任何多余的操作,ViewHolder则可以灵活地提供View,并将这些View贴到需要的地方。</p> <p>基于上述特征,我将固定的菜单栏设计为组合控件,提供各项菜单的tab,而将菜单的具体内容使用ViewHolder封装,在需要的时候从ViewHoder中拿到View,贴到我们需要放置的地方。同时,每个菜单中的UI逻辑也会被封装到ViewHolder中,这样,如果我们需要修改需求,直接改动对应的ViewHolder的代码,而不会影响其他代码。</p> <p>这样我们代码就可以将复杂的UI逻辑分成相互独立的小块,想改哪里改哪里,妈妈再也不用担心产品经理为难我了…………</p> <p>使用布局文件代替popupWindow</p> <p>翻阅网上很多仿制的美团菜单例程,几乎都没有真正和美团app的菜单一样,我们可以查看官方app,点击一个tab展开菜单,当在点击下一个tab时,菜单并没有收回,而是显示了当前tab对应的内容。</p> <p>由于很多demo都是使用popupWindow作为菜单的载体,而我在实际操作过程中发现popupWindow作为模态对话框非常难控制,而且还会引起其他问题,总之,我认为此处使用使用popupWindow并不合适。我在给菜单栏下面放了一块空布局,当向空布局中添加View时,空布局扩大,也就形成了下拉菜单的效果。</p> <p>那么有同学要问了,这样的话不就会影响下面的其他布局的位置了?是的,所以我们的菜单栏必须放在RelativeLayout或者FrameLayout这类结构中,而且必须放在其顶层。</p> <h2>控件原型</h2> <p>了解了上述两个问题的解决方法,我们就可以大概勾勒一样我们的控件大体的模样了。如下图:<br> <img alt="这里写图片描述" src="https://simg.open-open.com/show/b5c5d83443dbc0dec558a371bf42f02f.png"><br> 点击TAB1,TAB2,TAB3,内容View被对应的Holder中维护的View替换,我们清空内容View中的view时,由于这个View是包裹内容的,内容为空时高度自然变成0,也就是菜单收起的状态。我们可以为每个Holder设置相应的回调接口,这样我们的菜单View就能根据Holder的变化实时做出响应。</p> <h2>代码实现</h2> <h3>1.ViewHolder</h3> <p>ViewHolder用来维护一个我们手动inflate出来的View,并提供刷新数据的方法。我们可以以此为基类,封装UI逻辑,ViewHolder间也可以灵活替换。</p> <pre> <code class="language-java">/** * 自绘控件封装类 * 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; } }</code></pre> <h3>2.菜单View</h3> <p>这个View就是菜单栏主View,包括了三个TAB和下面的内容View,我们只需在工程中直接将这个类放入我们的布局文件中就可以了,注意,必须放在RelativieLayout或者FrameLayout中,而且必须是最顶层,否则内容View展开时会“挤”到其他布局。此处我们采用这种方式而不是popupWindow是因为popupWindow焦点改变可能会触发消失,这样无法实现点击不同的tab时,连续切换菜单的效果。</p> <pre> <code class="language-java">/** * * 搜索菜单栏 * 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"); } }</code></pre> <p>以下是demo的实现效果,点击不同tab,下面菜单实现连续切换:<br> <img alt="这里写图片描述" src="https://simg.open-open.com/show/e8d3e945494eaf51255f8222d9862033.png"></p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/0b56c5108d121cb3193ed4456059d9ac.png"></p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/be8ce1b1e7420d60171779b889f388e6.png"></p> <p>代码地址 <a href="/misc/goto?guid=4959671350643651454">https://git.oschina.net/vonchenchen/menu_demo.git</a></p> <p>下载地址 <a href="/misc/goto?guid=4959671350725021108">http://download.csdn.net/detail/lidec/9498648</a></p> <p>来自: <a href="/misc/goto?guid=4959671350821817899" rel="nofollow">http://blog.csdn.net/lidec/article/details/51205413</a></p>