Android 仿美团网,探索ListView的A-Z字母排序功能实现选择城市

gvvw1605 8年前
   <p>记得在我刚开始接触到美团网的时候就对美团网这个城市定位、选择城市功能很感兴趣,觉得它做得很棒。有如下几个点:</p>    <p>一:实现ListView的A-Z字母排序功能</p>    <p>二:根据输入框的输入值改变来过滤搜索结果,如果输入框里面的值为空,更新为原来的列表,否则为过滤数据列表</p>    <p>三:汉字转成拼音的功能,很多时候实现联系人或者城市列表等实现A-Z的排序功能,我们可以直接从数据库中获取他的汉字拼音,而对于一般的数据,我们怎么实现A-Z的排序,这里我使用了PinYin4j.jar将汉字转换为拼音.</p>    <p>按照惯例先来看一下最终效果图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0aef3afbcb6ff7b9583ddbd6185922f0.gif"></p>    <p style="text-align:center">这里写图片描述</p>    <p>接下来分析下整个功能模块的布局结构:</p>    <p>(1)首先一个带删除按钮的EditText,我们在输入框中输入我们查找的城市可以自动过滤出最终的结果,当输入框中没有数据自动替换到原来的数据列表;</p>    <p>(2)中间是当前定位的城市和热门的城市,其中热门城市使用到了GridView;</p>    <p>(3)下面是一个ListView用来显示数据列表,右侧是一个字母索引表,当我们点击不同的字母,ListView会定位到该字母地方</p>    <p>现在我们来看下项目结构图</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7b8a4f836f40eabf5882cfc182891e9c.png"></p>    <p style="text-align:center">这里写图片描述</p>    <p>按照项目中类的顺序来一一介绍</p>    <p>1.PinYin4j.jar用于将汉字转换为拼音,你还可以使用其他方式将汉子转换为拼音,我之前有介绍过,这里就不详讲啦。</p>    <p>2.CitySortModel一个实体类,一个显示的城市和相对应的拼音首字母</p>    <pre>  <code class="language-java">package com.adan.selectcitydome.view;    public class CitySortModel {        private String name;//显示的数据      private String sortLetters;//显示数据拼音的首字母        public String getName() {          return name;      }        public void setName(String name) {          this.name = name;      }        public String getSortLetters() {          return sortLetters;      }        public void setSortLetters(String sortLetters) {          this.sortLetters = sortLetters;      }  }</code></pre>    <p>3.EditTextWithDel类是自定义的一个带清除功能的输入框控件,也可以用Android原生的EditText,这个类上一篇博客有介绍,这里就不贴上代码了 <a href="/misc/goto?guid=4959716190758572871" rel="nofollow,noindex">Android 带清除功能的输入框控件EditTextWithDel</a></p>    <p>4.MyGridView类就是自定义GridView,主要是解决了在热门城市中嵌套Grideview的显示不完全的问题</p>    <pre>  <code class="language-java">package com.adan.selectcitydome.view;    import android.content.Context;  import android.util.AttributeSet;  import android.widget.GridView;    /**   * 自定义GridView,解决嵌套Grideview的显示不完全的问题   */  public class MyGridView extends GridView {        public MyGridView(Context context, AttributeSet attrs, int defStyle) {          super(context, attrs, defStyle);      }        public MyGridView(Context context, AttributeSet attrs) {          super(context, attrs);      }        public MyGridView(Context context) {          super(context);      }        /**       * 其中onMeasure函数决定了组件显示的高度与宽度;       * makeMeasureSpec函数中第一个函数决定布局空间的大小,第二个参数是布局模式       * MeasureSpec.AT_MOST的意思就是子控件需要多大的控件就扩展到多大的空间       */      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {            int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,                  MeasureSpec.AT_MOST);          super.onMeasure(widthMeasureSpec, expandSpec);      }    }</code></pre>    <p>5.PinyinComparator类用来对ListView中的数据根据A-Z进行排序,前面两个if判断主要是将不是以汉字开头的数据放在后面</p>    <pre>  <code class="language-java">package com.adan.selectcitydome.view;    import java.util.Comparator;    /**   * 用来对ListView中的数据根据A-Z进行排序,前面两个if判断主要是将不是以汉字开头的数据放在后面   */  public class PinyinComparator implements Comparator<CitySortModel> {        public int compare(CitySortModel o1, CitySortModel o2) {          //这里主要是用来对ListView里面的数据根据ABCDEFG...来排序          if (o1.getSortLetters().equals("@")                  || o2.getSortLetters().equals("#")) {              return -1;          } else if (o1.getSortLetters().equals("#")                  || o2.getSortLetters().equals("@")) {              return 1;          } else {              return o1.getSortLetters().compareTo(o2.getSortLetters());          }      }  }</code></pre>    <p>6.PinyinUtils类,就是第一点所讲的PinYin4j.jar用于将汉字转换为拼音啦,这里就不粘贴代码啦</p>    <p>7.SideBar类就是ListView右侧的字母索引View,我们需要使用setTextView(TextView mTextDialog)来设置用来显示当前按下的字母的TextView,以及使用setOnTouchingLetterChangedListener方法来设置回调接口,在回调方法onTouchingLetterChanged(String s)中来处理不同的操作</p>    <pre>  <code class="language-java">package com.adan.selectcitydome.view;    import android.content.Context;  import android.graphics.Canvas;  import android.graphics.Color;  import android.graphics.Paint;  import android.graphics.Typeface;  import android.util.AttributeSet;  import android.view.MotionEvent;  import android.view.View;  import android.widget.TextView;    import com.adan.selectcitydome.R;    import java.util.ArrayList;  import java.util.Arrays;  import java.util.List;    /**   * ListView右侧的字母索引View   */  public class SideBar extends View {        public static String[] INDEX_STRING = {"A", "B", "C", "D", "E", "F", "G", "H", "I",              "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",              "W", "X", "Y", "Z"};        private OnTouchingLetterChangedListener onTouchingLetterChangedListener;      private List<String> letterList;      private int choose = -1;      private Paint paint = new Paint();      private TextView mTextDialog;        public SideBar(Context context) {          this(context, null);      }        public SideBar(Context context, AttributeSet attrs) {          this(context, attrs, 0);      }        public SideBar(Context context, AttributeSet attrs, int defStyle) {          super(context, attrs, defStyle);          init();      }        private void init() {          setBackgroundColor(Color.parseColor("#F0F0F0"));          letterList = Arrays.asList(INDEX_STRING);      }        protected void onDraw(Canvas canvas) {          super.onDraw(canvas);          int height = getHeight();// 获取对应高度          int width = getWidth();// 获取对应宽度          int singleHeight = height / letterList.size();// 获取每一个字母的高度          for (int i = 0; i < letterList.size(); i++) {              paint.setColor(Color.parseColor("#606060"));              paint.setTypeface(Typeface.DEFAULT_BOLD);              paint.setAntiAlias(true);              paint.setTextSize(20);              // 选中的状态              if (i == choose) {                  paint.setColor(Color.parseColor("#4F41FD"));                  paint.setFakeBoldText(true);              }              // x坐标等于中间-字符串宽度的一半.              float xPos = width / 2 - paint.measureText(letterList.get(i)) / 2;              float yPos = singleHeight * i + singleHeight / 2;              canvas.drawText(letterList.get(i), xPos, yPos, paint);              paint.reset();// 重置画笔          }      }        @Override      public boolean dispatchTouchEvent(MotionEvent event) {          final int action = event.getAction();          final float y = event.getY();// 点击y坐标          final int oldChoose = choose;          final OnTouchingLetterChangedListener listener = onTouchingLetterChangedListener;          final int c = (int) (y / getHeight() * letterList.size());// 点击y坐标所占总高度的比例*b数组的长度就等于点击b中的个数.            switch (action) {              case MotionEvent.ACTION_UP:                  setBackgroundColor(Color.parseColor("#F0F0F0"));                  choose = -1;                  invalidate();                  if (mTextDialog != null) {                      mTextDialog.setVisibility(View.GONE);                  }                  break;              default:                  setBackgroundResource(R.drawable.sidebar_background);                  if (oldChoose != c) {                      if (c >= 0 && c < letterList.size()) {                          if (listener != null) {                              listener.onTouchingLetterChanged(letterList.get(c));                          }                          if (mTextDialog != null) {                              mTextDialog.setText(letterList.get(c));                              mTextDialog.setVisibility(View.VISIBLE);                          }                          choose = c;                          invalidate();                      }                  }                  break;          }          return true;      }        public void setIndexText(ArrayList<String> indexStrings) {          this.letterList = indexStrings;          invalidate();      }        /**       * 为SideBar设置显示当前按下的字母的TextView       *       * @param mTextDialog       */      public void setTextView(TextView mTextDialog) {          this.mTextDialog = mTextDialog;      }        /**       * 向外公开的方法       *       * @param onTouchingLetterChangedListener       */      public void setOnTouchingLetterChangedListener(              OnTouchingLetterChangedListener onTouchingLetterChangedListener) {          this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;      }        /**       * 接口       */      public interface OnTouchingLetterChangedListener {          void onTouchingLetterChanged(String s);      }    }</code></pre>    <p>8.CityAdapter就是热门城市中GridView的适配器</p>    <pre>  <code class="language-java">package com.adan.selectcitydome;    import android.content.Context;  import android.view.LayoutInflater;  import android.view.View;  import android.view.ViewGroup;  import android.widget.ArrayAdapter;  import android.widget.Button;  import android.widget.LinearLayout;    import java.util.List;    /**   * @author: xiaolijuan   * @description:   * @projectName: SelectCityDome   * @date: 2016-03-01   * @time: 17:25   */  public class CityAdapter extends ArrayAdapter<String> {      /**       * 需要渲染的item布局文件       */      private int resource;        public CityAdapter(Context context, int textViewResourceId, List<String> objects) {          super(context, textViewResourceId, objects);          resource = textViewResourceId;      }        @Override      public View getView(int position, View convertView, ViewGroup parent) {          LinearLayout layout = null;          if (convertView == null) {              layout = (LinearLayout) LayoutInflater.from(getContext()).inflate(resource, null);          } else {              layout = (LinearLayout) convertView;          }          Button name = (Button) layout.findViewById(R.id.tv_city);          name.setText(getItem(position));          return layout;      }  }</code></pre>    <p>9.SortAdapter 数据的适配器类,这里我们需要用到的就是SectionIndexer接口,它能够有效地帮助我们对分组进行控制。使用SectionIndexer接口需要实现三个方法:getSectionForPosition(int position),getPositionForSection(int section),getSections(),我们只需要自行实现前面两个方法:</p>    <p>(一)getSectionForPosition(int position)是根据ListView的position来找出当前位置所在的分组</p>    <p>getPositionForSection(int section)就是根据首字母的Char值来获取在该ListView中第一次出现该首字母的位置,也就是当前分组所在的位置</p>    <pre>  <code class="language-java">package com.adan.selectcitydome;    import android.content.Context;  import android.view.LayoutInflater;  import android.view.View;  import android.view.ViewGroup;  import android.widget.BaseAdapter;  import android.widget.SectionIndexer;  import android.widget.TextView;    import com.adan.selectcitydome.view.CitySortModel;    import java.util.List;    public class SortAdapter extends BaseAdapter implements SectionIndexer {      private List<CitySortModel> list = null;      private Context mContext;        public SortAdapter(Context mContext, List<CitySortModel> list) {          this.mContext = mContext;          this.list = list;      }        /**       * 当ListView数据发生变化时,调用此方法来更新ListView       *       * @param list       */      public void updateListView(List<CitySortModel> list) {          this.list = list;          notifyDataSetChanged();      }        public int getCount() {          return this.list.size();      }        public Object getItem(int position) {          return list.get(position);      }        public long getItemId(int position) {          return position;      }        public View getView(final int position, View view, ViewGroup arg2) {          ViewHolder viewHolder = null;          final CitySortModel mContent = list.get(position);          if (view == null) {              viewHolder = new ViewHolder();              view = LayoutInflater.from(mContext).inflate(R.layout.item_select_city, null);              viewHolder.tvTitle = (TextView) view.findViewById(R.id.tv_city_name);              view.setTag(viewHolder);              viewHolder.tvLetter = (TextView) view.findViewById(R.id.tv_catagory);          } else {              viewHolder = (ViewHolder) view.getTag();          }            int section = getSectionForPosition(position);            if (position == getPositionForSection(section)) {              viewHolder.tvLetter.setVisibility(View.VISIBLE);              viewHolder.tvLetter.setText(mContent.getSortLetters());          } else {              viewHolder.tvLetter.setVisibility(View.GONE);          }            viewHolder.tvTitle.setText(this.list.get(position).getName());            return view;        }          final static class ViewHolder {          TextView tvLetter;          TextView tvTitle;      }        public int getSectionForPosition(int position) {          return list.get(position).getSortLetters().charAt(0);      }        public int getPositionForSection(int section) {          for (int i = 0; i < getCount(); i++) {              String sortStr = list.get(i).getSortLetters();              char firstChar = sortStr.toUpperCase().charAt(0);              if (firstChar == section) {                  return i;              }          }          return -1;      }        @Override      public Object[] getSections() {          return null;      }  }</code></pre>    <p>10.MainActivity 对EditTextWithDel设置addTextChangedListener监听,当输入框内容发生变化根据里面的值过滤ListView,里面的值为空显示原来的列表和给ListView添加表头等</p>    <pre>  <code class="language-java">package com.adan.selectcitydome;    import android.app.Activity;  import android.os.Bundle;  import android.text.Editable;  import android.text.TextUtils;  import android.text.TextWatcher;  import android.view.View;  import android.widget.AdapterView;  import android.widget.GridView;  import android.widget.ListView;  import android.widget.TextView;  import android.widget.Toast;    import com.adan.selectcitydome.view.CitySortModel;  import com.adan.selectcitydome.view.EditTextWithDel;  import com.adan.selectcitydome.view.PinyinComparator;  import com.adan.selectcitydome.view.PinyinUtils;  import com.adan.selectcitydome.view.SideBar;    import java.util.ArrayList;  import java.util.Collections;  import java.util.List;    public class MainActivity extends Activity {      private ListView sortListView;      private SideBar sideBar;      private TextView dialog, mTvTitle;      private SortAdapter adapter;      private EditTextWithDel mEtCityName;      private List<CitySortModel> SourceDateList;        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          initViews();      }        private void initViews() {          mEtCityName = (EditTextWithDel) findViewById(R.id.et_search);          sideBar = (SideBar) findViewById(R.id.sidrbar);          dialog = (TextView) findViewById(R.id.dialog);          mTvTitle = (TextView) findViewById(R.id.tv_title);          sortListView = (ListView) findViewById(R.id.country_lvcountry);          initDatas();          initEvents();          setAdapter();      }        private void setAdapter() {          SourceDateList = filledData(getResources().getStringArray(R.array.provinces));          Collections.sort(SourceDateList, new PinyinComparator());          adapter = new SortAdapter(this, SourceDateList);          sortListView.addHeaderView(initHeadView());          sortListView.setAdapter(adapter);      }        private void initEvents() {          //设置右侧触摸监听          sideBar.setOnTouchingLetterChangedListener(new SideBar.OnTouchingLetterChangedListener() {              @Override              public void onTouchingLetterChanged(String s) {                  //该字母首次出现的位置                  int position = adapter.getPositionForSection(s.charAt(0));                  if (position != -1) {                      sortListView.setSelection(position + 1);                  }              }          });            //ListView的点击事件          sortListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {                @Override              public void onItemClick(AdapterView<?> parent, View view,                                      int position, long id) {                  mTvTitle.setText(((CitySortModel) adapter.getItem(position - 1)).getName());                  Toast.makeText(getApplication(), ((CitySortModel) adapter.getItem(position)).getName(), Toast.LENGTH_SHORT).show();              }          });            //根据输入框输入值的改变来过滤搜索          mEtCityName.addTextChangedListener(new TextWatcher() {              @Override              public void beforeTextChanged(CharSequence s, int start, int count, int after) {                }                @Override              public void onTextChanged(CharSequence s, int start, int before, int count) {                  //当输入框里面的值为空,更新为原来的列表,否则为过滤数据列表                  filterData(s.toString());              }                @Override              public void afterTextChanged(Editable s) {                }          });      }        private void initDatas() {          sideBar.setTextView(dialog);      }        private View initHeadView() {          View headView = getLayoutInflater().inflate(R.layout.headview, null);          GridView mGvCity = (GridView) headView.findViewById(R.id.gv_hot_city);          String[] datas = getResources().getStringArray(R.array.city);          ArrayList<String> cityList = new ArrayList<>();          for (int i = 0; i < datas.length; i++) {              cityList.add(datas[i]);          }          CityAdapter adapter = new CityAdapter(getApplicationContext(), R.layout.gridview_item, cityList);          mGvCity.setAdapter(adapter);          return headView;      }        /**       * 根据输入框中的值来过滤数据并更新ListView       *       * @param filterStr       */      private void filterData(String filterStr) {          List<CitySortModel> mSortList = new ArrayList<>();          if (TextUtils.isEmpty(filterStr)) {              mSortList = SourceDateList;          } else {              mSortList.clear();              for (CitySortModel sortModel : SourceDateList) {                  String name = sortModel.getName();                  if (name.toUpperCase().indexOf(filterStr.toString().toUpperCase()) != -1 || PinyinUtils.getPingYin(name).toUpperCase().startsWith(filterStr.toString().toUpperCase())) {                      mSortList.add(sortModel);                  }              }          }          // 根据a-z进行排序          Collections.sort(mSortList, new PinyinComparator());          adapter.updateListView(mSortList);      }        private List<CitySortModel> filledData(String[] date) {          List<CitySortModel> mSortList = new ArrayList<>();          ArrayList<String> indexString = new ArrayList<>();            for (int i = 0; i < date.length; i++) {              CitySortModel sortModel = new CitySortModel();              sortModel.setName(date[i]);              String pinyin = PinyinUtils.getPingYin(date[i]);              String sortString = pinyin.substring(0, 1).toUpperCase();              if (sortString.matches("[A-Z]")) {                  sortModel.setSortLetters(sortString.toUpperCase());                  if (!indexString.contains(sortString)) {                      indexString.add(sortString);                  }              }              mSortList.add(sortModel);          }          Collections.sort(indexString);          sideBar.setIndexText(indexString);          return mSortList;      }  }</code></pre>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/350f7df569fc</p>    <p> </p>