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的布局

    android:layout_width="match_parent"
    android:layout_height="match_parent">

            android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none" />
   
            android:id="@+id/words"
        android:layout_width="30dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true" />
   
            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" />

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

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

   
            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" />

   
            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" />


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() {
            @Override
            public int compare(Person lhs, Person rhs) {
                //根据拼音进行排序
                return lhs.getPinyin().compareTo(rhs.getPinyin());
            }
        });
    }

6.撸ListView的列表适配器
public class MyAdapter extends BaseAdapter {
    private List list;
    private LayoutInflater inflater;

    public MyAdapter(Context context, List 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://download.csdn.net/download/qq_21445563/10922335

你可能感兴趣的:(Android)