Android自定義View——實現聯繫人列表字母索引

相信大家對這個列表字母索引已經不陌生了,在很多app中也隨處可見,像沒團的城市地址選擇,微信聯繫人列表,手機通訊錄…等等。既然是個這麼nb這麼實用的功能我們怎麼能不Get到來呢,下面就讓我們一起造一個出來吧


一:我們可以大致將他分成3小塊,右邊的字母列表、中央的當前字母提示、ListView列表。ok分析好了那我們就一步步來編碼實現吧
二:首先來實現右邊的字母列表
1. 在畫這個字母列表之前,先畫張圖來大致計算一下字母的座標,如下圖:


    /*繪製的列表導航字母*/
    private String words[] = {"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 Paint wordsPaint;
    /*字母背景畫筆*/
    private Paint bgPaint;
    /*每一個字母的寬度*/
    private int itemWidth;
    /*每一個字母的高度*/
    private int itemHeight;
    /*手指按下的字母索引*/
    private int touchIndex = 0;
    /*手指按下的字母改變接口*/
    private onWordsChangeListener listener;

//得到畫布的寬度和每一個字母所佔的高度
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        itemWidth = getMeasuredWidth();
        //使得邊距好看一些
        int height = getMeasuredHeight() - 10;
        itemHeight = height / 27;
    }

2. 開始繪製A~Z~#的字符,先繪製字母背景,在繪製文字
@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = 0; i < words.length; i++) {
            //判斷是不是我們按下的當前字母
            if (touchIndex == i) {
                //繪製文字圓形背景
                canvas.drawCircle(itemWidth / 2, itemHeight / 2 + i * itemHeight, 23, bgPaint);
                wordsPaint.setColor(Color.WHITE);
            } else {
                wordsPaint.setColor(Color.GRAY);
            }
            //獲取文字的寬高
            Rect rect = new Rect();
            wordsPaint.getTextBounds(words[i], 0, 1, rect);
            int wordWidth = rect.width();
            //繪製字母
            float wordX = itemWidth / 2 - wordWidth / 2;
            float wordY = itemWidth / 2 + i * itemHeight;
            canvas.drawText(words[i], wordX, wordY, wordsPaint);
        }
    }

3.現在效果就是這個樣子了


4. 現在來實現手指滑動或者點擊字母列表的時候來改變當前選中的字母和在屏幕中央進行顯示。這裏怎麼實現呢?很容易就想到這裏肯定是在onTouchEvent中做處理,在使用接口回調來在屏幕中央顯示當前字母
    /**
     * 當手指觸摸按下的時候改變字母背景顏色
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                float y = event.getY();
                //關鍵點===獲得我們按下的是那個索引(字母)
                int index = (int) (y / itemHeight);
                if (index != touchIndex)
                    touchIndex = index;
                //防止數組越界
                if (listener != null && 0 <= touchIndex && touchIndex <= words.length - 1) {
                    //回調按下的字母
                    listener.wordsChange(words[touchIndex]);
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                //手指擡起,不做任何操作
                break;
        }
        return true;
    }

    /*手指按下了哪個字母的回調接口*/
    public interface onWordsChangeListener {
        void wordsChange(String words);
    }

    /*設置手指按下字母改變監聽*/
    public void setOnWordsChangeListener(onWordsChangeListener listener) {
        this.listener = listener;
    }

5.接口都寫好了就可以在主界面中來顯示了,activity_main的佈局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none" />
    <!--字母導航-->
    <com.zsy.words.view.WordsNavigation
        android:id="@+id/words"
        android:layout_width="30dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true" />
    <!--這個就用來顯示我們當前按下的字母-->
    <TextView
        android:id="@+id/tv"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:background="@drawable/tvstyle"
        android:gravity="center"
        android:textSize="40sp"
        android:visibility="gone" />
</RelativeLayout

6.MainActivity中設置字母改變監聽
tv = (TextView) findViewById(R.id.tv);
word = (WordsNavigation) findViewById(R.id.words);
word.setOnWordsChangeListener(this);

    //手指按下字母改變監聽回調
    @Override
    public void wordsChange(String words) {
        updateWord(words);
    }

    /**
     * 更新中央的字母提示
     *
     * @param words 首字母
     */
    private void updateWord(String words) {
        tv.setText(words);
        tv.setVisibility(View.VISIBLE);
        //清空之前的所有消息
        handler.removeCallbacksAndMessages(null);
        //500ms後讓tv隱藏
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                tv.setVisibility(View.GONE);
            }
        }, 500);
    }

三:現在就只剩下ListView列表,和滑動列表來改變字母的背景了同時滑動列表來改變我們listView的數據顯示
1.爲ListView數據添加一個實體類Person,這裏我們需要對我們的姓名轉化成拼音,我這裏使用的是pinyin4j-2.5.0.jar鏈接文章末尾給出
/*
 * 文件名:     Person
 * 創建者:     阿鍾
 * 創建時間:   2016/11/17 19:07
 * 描述:       封裝聯繫人列表信息
 */
public class Person {
    //姓名
    private String name;
    //拼音
    private String pinyin;
    //拼音首字母
    private String headerWord;

    public Person(String name) {
        this.name = name;
        this.pinyin = PinYinUtils.getPinyin(name);
        headerWord = pinyin.substring(0, 1);
    }

    public String getPinyin() {
        return pinyin;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getHeaderWord() {
        return headerWord;
    }
}

2.PinYinUtils將漢子轉拼音
/*
 * 文件名:   PinYinUtils
 * 創建者:   ZSY
 * 創建時間: 2016/11/17 17:51
 * 描述:     得到指定漢字的拼音
 */
public class PinYinUtils {
    /**
     * 將hanzi轉成拼音
     *
     * @param hanzi 漢字或字母
     * @return 拼音
     */
    public static String getPinyin(String hanzi) {
        StringBuilder sb = new StringBuilder();
        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
        format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        //由於不能直接對多個漢子轉換,只能對單個漢子轉換
        char[] arr = hanzi.toCharArray();
        for (int i = 0; i < arr.length; i++) {
            if (Character.isWhitespace(arr[i])) {
                continue;
            }
            try {
                String[] pinyinArr = PinyinHelper.toHanyuPinyinStringArray(arr[i], format);
                if (pinyinArr != null) {
                    sb.append(pinyinArr[0]);
                } else {
                    sb.append(arr[i]);
                }
            } catch (Exception e) {
                e.printStackTrace();
                //不是正確的漢字
                sb.append(arr[i]);
            }

        }
        return sb.toString();
    }
}

3.現在我們就來擼ListView的Item佈局了list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!--顯示字母-->
    <TextView
        android:id="@+id/tv_word"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:background="#ebebeb"
        android:gravity="center_vertical"
        android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" />

    <!--顯示聯繫人信息-->
    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:minHeight="?android:attr/listPreferredItemHeightSmall"
        android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
        android:paddingRight="?android:attr/listPreferredItemPaddingRight"
        android:textAppearance="?android:attr/textAppearanceListItemSmall" />
</LinearLayout>

5.爲我們的列表添加測試數據,並對數據進行排序
    /**
     * 初始化聯繫人列表信息
     */
    private void initData() {
        list = new ArrayList<>();
        list.add(new Person("Dave"));
        list.add(new Person("阿鍾"));
        //省略一些....
        list.add(new Person("胡繼羣"));
        list.add(new Person("隔壁老王"));
        list.add(new Person("姜宇航"));

        //對集合排序
        Collections.sort(list, new Comparator<Person>() {
            @Override
            public int compare(Person lhs, Person rhs) {
                //根據拼音進行排序
                return lhs.getPinyin().compareTo(rhs.getPinyin());
            }
        });
    }

6.擼ListView的列表適配器
public class MyAdapter extends BaseAdapter {
    private List<Person> list;
    private LayoutInflater inflater;

    public MyAdapter(Context context, List<Person> list) {
        inflater = LayoutInflater.from(context);
        this.list = list;
    }

    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = inflater.inflate(R.layout.list_item, null);
            holder.tv_word = (TextView) convertView.findViewById(R.id.tv_word);
            holder.tv_name = (TextView) convertView.findViewById(R.id.tv_name);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        String word = list.get(position).getHeaderWord();
        holder.tv_word.setText(word);
        holder.tv_name.setText(list.get(position).getName());
        //將相同字母開頭的合併在一起
        if (position == 0) {
            //第一個是一定顯示的
            holder.tv_word.setVisibility(View.VISIBLE);
        } else {
            //後一個與前一個對比,判斷首字母是否相同,相同則隱藏
            String headerWord = list.get(position - 1).getHeaderWord();
            if (word.equals(headerWord)) {
                holder.tv_word.setVisibility(View.GONE);
            } else {
                holder.tv_word.setVisibility(View.VISIBLE);
            }
        }
        return convertView;
    }

    private class ViewHolder {
        private TextView tv_word;
        private TextView tv_name;
    }
}

7.現在我們需要來實現,當滑動列表的時候需要更新ListView的顯示,在wordsChange函數中進行出理,同時滑動列表我們也需要更新右側字母列表的狀態,ok邏輯理清了就好辦事了。
1.在wordsChange調用此函數updateListView改變ListView的顯示
    /**
     * @param words 首字母
     */
    private void updateListView(String words) {
        for (int i = 0; i < list.size(); i++) {
            String headerWord = list.get(i).getHeaderWord();
            //將手指按下的字母與列表中相同字母開頭的項找出來
            if (words.equals(headerWord)) {
                //將列表選中哪一個
                listView.setSelection(i);
                //找到開頭的一個即可
                return;
            }
        }
    }

2.爲ListView設置滑動監聽,來改變右側字母列表的狀態
@Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        //當滑動列表的時候,更新右側字母列表的選中狀態
        word.setTouchIndex(list.get(firstVisibleItem).getHeaderWord());
    }

3.自定義字母列表View中的函數,這樣當我們滑動列表的時候字母狀態也就可以時時同步了
    /*設置當前按下的是那個字母*/
    public void setTouchIndex(String word) {
        for (int i = 0; i < words.length; i++) {
            if (words[i].equals(word)) {
                touchIndex = i;
                invalidate();
                return;
            }
        }
    }

ok大功告成,一個簡單的聯繫字母索引列表就實現了。

相關代碼如下:

https://github.com/liyanmei/ContactsList

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章