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>