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>