1. 程式人生 > >Android 仿美團網,探索ListView的A-Z字母排序功能實現選擇城市

Android 仿美團網,探索ListView的A-Z字母排序功能實現選擇城市

記得在我剛開始接觸到美團網的時候就對美團網這個城市定位、選擇城市功能很感興趣,覺得它做得很棒。有如下幾個點:
一:實現ListView的A-Z字母排序功能
二:根據輸入框的輸入值改變來過濾搜尋結果,如果輸入框裡面的值為空,更新為原來的列表,否則為過濾資料列表
三:漢字轉成拼音的功能,很多時候實現聯絡人或者城市列表等實現A-Z的排序功能,我們可以直接從資料庫中獲取他的漢字拼音,而對於一般的資料,我們怎麼實現A-Z的排序,這裡我使用了PinYin4j.jar將漢字轉換為拼音.
按照慣例先來看一下最終效果圖:
這裡寫圖片描述
接下來分析下整個功能模組的佈局結構:
(1)首先一個帶刪除按鈕的EditText,我們在輸入框中輸入我們查詢的城市可以自動過濾出最終的結果,當輸入框中沒有資料自動替換到原來的資料列表;
(2)中間是當前定位的城市和熱門的城市,其中熱門城市使用到了GridView;
(3)下面是一個ListView用來顯示資料列表,右側是一個字母索引表,當我們點選不同的字母,ListView會定位到該字母地方
現在我們來看下專案結構圖
這裡寫圖片描述


按照專案中類的順序來一一介紹
1.PinYin4j.jar用於將漢字轉換為拼音,你還可以使用其他方式將漢子轉換為拼音,我之前有介紹過,這裡就不詳講啦,探索PinYin4j.jar將漢字轉換為拼音的基本用法
2.CitySortModel一個實體類,一個顯示的城市和相對應的拼音首字母

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; } }

3.EditTextWithDel類是自定義的一個帶清除功能的輸入框控制元件,也可以用Android原生的EditText,這個類上一篇部落格有介紹,這裡就不貼上程式碼了

Android 帶清除功能的輸入框控制元件EditTextWithDel
4.MyGridView類就是自定義GridView,主要是解決了在熱門城市中巢狀Grideview的顯示不完全的問題

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);
    }

}

5.PinyinComparator類用來對ListView中的資料根據A-Z進行排序,前面兩個if判斷主要是將不是以漢字開頭的資料放在後面

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());
        }
    }
}

6.PinyinUtils類,就是第一點所講的PinYin4j.jar用於將漢字轉換為拼音啦,這裡就不貼上程式碼啦
7.SideBar類就是ListView右側的字母索引View,我們需要使用setTextView(TextView mTextDialog)來設定用來顯示當前按下的字母的TextView,以及使用setOnTouchingLetterChangedListener方法來設定回撥介面,在回撥方法onTouchingLetterChanged(String s)中來處理不同的操作

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);
    }

}

8.CityAdapter就是熱門城市中GridView的介面卡

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;
    }
}

9.SortAdapter 資料的介面卡類,這裡我們需要用到的就是SectionIndexer介面,它能夠有效地幫助我們對分組進行控制。使用SectionIndexer介面需要實現三個方法:getSectionForPosition(int position),getPositionForSection(int section),getSections(),我們只需要自行實現前面兩個方法:
(一)getSectionForPosition(int position)是根據ListView的position來找出當前位置所在的分組
getPositionForSection(int section)就是根據首字母的Char值來獲取在該ListView中第一次出現該首字母的位置,也就是當前分組所在的位置

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;
    }
}

10.MainActivity 對EditTextWithDel設定addTextChangedListener監聽,當輸入框內容發生變化根據裡面的值過濾ListView,裡面的值為空顯示原來的列表和給ListView新增表頭等

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;
    }
}