Android自定义View实现快速索引(如微信好友列表,通讯录)

①最右侧的索引是用自定义View来实现的,通过onDraw方法将其描绘;②用pinyin4j-2.5.0.jar第三方架包取到每个名字的首字母,将汉字转化成拼音再取第一个字符;③ListView的adapte适配器。如下图所示:
Android自定义View实现快速索引(如微信好友列表,通讯录)

1 布局实现

单个记录的实现,代码如下:
item_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/tv_word"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#44000000"
        android:text="A"
        android:textColor="#000000"
        android:textSize="25sp" />
    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="阿三"
        android:textColor="#000000"
        android:textSize="25sp" />
</LinearLayout>

主页面布局的实现,代码如下
activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.atguigu.quickindex.MainActivity">
    <!--左边内容-->
    <ListView
        android:id="@+id/lv_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <!--中间内容-->
    <TextView
        android:id="@+id/tv_word"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_centerInParent="true"
        android:background="#44000000"
        android:gravity="center"
        android:text="A"
        android:textColor="#000000"
        android:textSize="30sp"
        android:visibility="gone" />
    <!--右边内容-->
    <com.wang.quickindex.IndexView
        android:id="@+id/iv_words"
        android:layout_width="30dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        android:background="#ff0000" />
</RelativeLayout>

2 代码功能实现

自定义的View来实现快速索引,代码如下:

/**
 * 作用:快速索引,绘制26个字母
 * 1.把26个字母放入数组
 * 2.在onMeasure计算每条的高itemHeight和宽itemWidth,
 * 3.在onDraw和wordWidth,wordHeight,wordX,wordY
 *
 * 手指按下文字变色
 * 1.重写onTouchEvent(),返回true,在down/move的过程中计算
 * int touchIndex = Y / itemHeight; 强制绘制
 *
 * 2.在onDraw()方法对于的下标设置画笔变色
 *
 * 3.在up的时候
 * touchIndex  = -1; //还原默认
 * 强制绘制
 */

public class IndexView extends View {
    /**
     * 每条的宽和高
     */
    private int itemWidth;
    private int itemHeight;

    private Paint paint;  //画笔

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

    /**
     * 如果我们在布局文件使用该类,将会用这个构造方法实例该类,如果没有就会崩溃
     *
     * @param context
     * @param attrs
     */
    public IndexView(Context context, @Nullable AttributeSet attrs) {  //要实现带有两个参数值的构造方法
        super(context, attrs);
        paint = new Paint();
        paint.setTextSize(40);  //字母字体大小
        paint.setColor(Color.WHITE); //默认设置画笔是白色
        paint.setAntiAlias(true); //设置抗锯齿
        paint.setTypeface(Typeface.DEFAULT_BOLD);//设置粗体字
    }

    /**
     * 测量方法
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        itemWidth = getMeasuredWidth();
        itemHeight = getMeasuredHeight() / words.length;  //words.length是26。除以26
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = 0; i<words.length;i++){
            if (touchIndex == i){
                //设置为灰色(按下去的时候)
                paint.setColor(Color.GRAY);
            }else {  //没有-1走下面代码
                //设置为白色(松开)
                paint.setColor(Color.WHITE);
            }

            String word = words[i];//若取值是A

            Rect rect = new Rect();  //矩形
            //画笔
            paint.getTextBounds(word, 0, 1, rect);  // 0, 1指的是取一个字母

            //字母的高和宽
            int wordWidth = rect.width();
            int wordHeight = rect.height();

            //计算每个字母在视图上的坐标位置
            float wordX = itemWidth / 2 - wordWidth / 2;
            float wordY = itemHeight / 2 + wordHeight / 2 + i * itemHeight;  ///i * itemHeight是指往下绘制字母

            canvas.drawText(word, wordX, wordY, paint);
        }
    }

    private int touchIndex = -1; //默认字母的下标位置

    /**
     * 手指按下文字变色
     * 1.重写onTouchEvent(),返回true,在down/move的过程中计算
     * int touchIndex = Y / itemHeight; 强制绘制
     *
     * 2.在onDraw()方法对于的下标设置画笔变色
     *
     * 3.在up的时候
     * touchIndex  = -1; //还原默认
     * 强制绘制
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(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;  //当前的索引位置
                    invalidate(); //强制绘制会导致OnDraw()方法执行
                    if (onIndexChangeListener != null && touchIndex < words.length){
                        onIndexChangeListener.onIndexChange(words[touchIndex]);
                    }
                }
                break;

            case MotionEvent.ACTION_UP:
                touchIndex = -1;  //还原
                invalidate();
                break;
        }
        return true;
    }

    /**
     * 字母下标索引变化的监听器(做接口)
     */
    public interface OnIndexChangeListener{

        /**
         * 当字母下标位置发生变化的时候回调
         * @param word 字母(A~Z)
         */
        void onIndexChange(String word); //没有返回值
    }

    private OnIndexChangeListener onIndexChangeListener;

    /** Setter方法
     * 设置字母下标索引变化的监听
     * @param onIndexChangeListener
     */
    public void setOnIndexChangeListener(OnIndexChangeListener onIndexChangeListener) {
        this.onIndexChangeListener = onIndexChangeListener;
    }
}

实体类的实现,代码如下

/**
 * 作用: 姓名 :阿三
 *       拼音:ASAN
 */

public class Person {

    private String name;

    private String pinyin;

    public Person(String name) {  //生成带name参数的构造方法
        this.name = name;
        this.pinyin = PinYinUtils.getPinYin(name);
    }

    //Getter和Setter方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPinyin() {
        return pinyin;
    }
    public void setPinyin(String pinyin) {
        this.pinyin = pinyin;
    }

    //toString方法 (Alt+Insert)
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", pinyin='" + pinyin + '\'' +
                '}';
    }
}

汉字转成拼音的实现,代码如下:

/**
 * 作用:把汉字转换成拼音
 * 阿三
 * ASAN
 */
public class PinYinUtils {
    /**
     * 得到指定汉字的拼音
     * 注意:不应该被频繁调用,它消耗一定内存
     * @param hanzi
     * @return
     */
    public static String getPinYin(String hanzi){
        String pinyin = "";

        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();//控制转换是否大小写,是否带音标
        format.setCaseType(HanyuPinyinCaseType.UPPERCASE);//大写是UPPERCASE;小写是LOWERCASE
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);

        //由于不能直接对多个汉字转换,只能对单个汉字转换
        char[] arr = hanzi.toCharArray();
        for (int i = 0; i < arr.length; i++) {
            if(Character.isWhitespace(arr[i]))continue;//如果是空格,则不处理,进行下次遍历

            //汉字是2个字节存储,肯定大于127,所以大于127就可以当为汉字转换
            if(arr[i]>127){
                try {
                    //由于多音字的存在,单 dan shan
                    String[] pinyinArr = PinyinHelper.toHanyuPinyinStringArray(arr[i], format);

                    if(pinyinArr!=null){
                        pinyin += pinyinArr[0];
                    }else {
                        pinyin += arr[i];
                    }
                } catch (BadHanyuPinyinOutputFormatCombination e) {
                    e.printStackTrace();
                    //不是正确的汉字
                    pinyin += arr[i];
                }
            }else {
                //不是汉字,
                pinyin += arr[i];
            }
        }
        return pinyin;
    }
}

主代码的实现,代码如下:

public class MainActivity extends Activity {
    //实例化
    private ListView lv_main;
    private TextView tv_word;
    private IndexView iv_words;

    private Handler handler = new Handler(); //用于隐藏切换后的字母,在主线程中运行

    /**
     * 联系人集合
     */
    private ArrayList<Person> persons;

    private IndexAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //实例化
        lv_main= (ListView)findViewById(R.id.lv_main);
        tv_word= (TextView)findViewById(R.id.tv_word);
        iv_words= (IndexView) findViewById(R.id.iv_words);

//        //方法一:设置监听字母下标索引的变化
//        iv_words.setOnIndexChangeListener(new IndexView.OnIndexChangeListener() {  //写的内部类(OnIndexChangeListener)
//            /**
//             *
//             * @param word 字母(A~Z)
//             */
//            @Override
//            public void onIndexChange(String word) {
//                updateWord(word);
//
//            }
//        });

        //设置监听字母下标索引的变化
        iv_words.setOnIndexChangeListener(new MyOnIndexChangeListener());

        //准备数据
        initData();

        //设置适配器
        adapter = new IndexAdapter();  //最好适配器new成变量,以后就不用改了
        lv_main.setAdapter(adapter);
    }

    class IndexAdapter extends BaseAdapter{
        @Override
        public int getCount() {
            return persons.size();
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder;
            if (convertView == null){
                convertView = View.inflate(MainActivity.this,R.layout.item_main,null);

                viewHolder = new ViewHolder();
                viewHolder.tv_word = (TextView)convertView.findViewById(R.id.tv_word);
                viewHolder.tv_name = (TextView)convertView.findViewById(R.id.tv_name);
                convertView.setTag(viewHolder);  //有set就有get
            }else {
                viewHolder = (ViewHolder) convertView.getTag();
            }

            //得到姓名和拼音
            String name = persons.get(position).getName(); //阿福
            String word = persons.get(position).getPinyin().substring(0,1); //AFU变成A(substring(0,1),截取只剩下一个A
            viewHolder.tv_word.setText(word);
            viewHolder.tv_name.setText(name);

            if (position == 0){  //若每种信息字母的第一行显示
                viewHolder.tv_word.setVisibility(View.VISIBLE); //显示
            }else {
                //得到前一个位置对应的字母,如果当前的字母和上一个相同,隐藏TextView;否则就显示
                String preWord = persons.get(position - 1).getPinyin().substring(0, 1); //得到上一个字母A~Z
                if (word.equals(preWord)){  //若word和preWord相同
                    viewHolder.tv_word.setVisibility(View.GONE); //隐藏
                }else {
                    viewHolder.tv_word.setVisibility(View.VISIBLE); //显示
                }
            }

            return convertView;
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

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

    //优化
    static class ViewHolder{
        TextView tv_word;
        TextView tv_name;
    }

    //方法二:设置监听字母下标索引的变化
    class MyOnIndexChangeListener implements IndexView.OnIndexChangeListener {
        /**
         * @param word 字母(A~Z)
         */
        @Override
        public void onIndexChange(String word) {
            updateWord(word);
            updateListView(word);//A~Z字母
        }
    }

    private void updateListView(String word) {
        for (int i =0;i<persons.size();i++){
            String listWord = persons.get(i).getPinyin().substring(0,1); //YANGGUANGFU-(转成)->Y
            if (word.equals(listWord)){
                //i是ListView中的位置
                lv_main.setSelection(i);  //定位到ListVeiw中的某个位置
                return;
            }
        }
    }

    private void updateWord(String word) {
        //显示
        tv_word.setVisibility(View.VISIBLE);
        tv_word.setText(word);
        handler.removeCallbacksAndMessages(null); //先把每次的消息移除
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //因为handler在主线程中运行,Runnable方法也是运行在主线程
                //(打日志System.out.println是判断Runnable在哪个线程中运行,出现main证明就是在主线程中运行)
                System.out.println(Thread.currentThread().getName() +"-------------------");
                tv_word.setVisibility(View.GONE); ////3秒后隐藏
            }
        },3000); //3秒后隐藏
    }

    /**
     * 初始化数据
     */
    private void initData() {

        persons = new ArrayList<>();
        persons.add(new Person("张小光"));  //将人名添加到集合中
        persons.add(new Person("杨大雷"));
        persons.add(new Person("胡继开"));
        persons.add(new Person("刘三"));

        persons.add(new Person("钟兴"));
        persons.add(new Person("尹顺"));
        persons.add(new Person("安杰"));
        persons.add(new Person("张骞"));

        persons.add(new Person("温小松"));
        persons.add(new Person("李凤"));
        persons.add(new Person("杜甫"));
        persons.add(new Person("娄志超"));
        persons.add(new Person("张飞"));

        persons.add(new Person("王杰"));
        persons.add(new Person("李三"));
        persons.add(new Person("孙二娘"));
        persons.add(new Person("唐小雷"));
        persons.add(new Person("牛二"));
        persons.add(new Person("姜光刃"));

        persons.add(new Person("刘能"));
        persons.add(new Person("张四"));
        persons.add(new Person("张五"));
        persons.add(new Person("侯大帅"));
        persons.add(new Person("刘洪"));

        persons.add(new Person("乔三"));
        persons.add(new Person("徐达健"));
        persons.add(new Person("吴洪亮"));
        persons.add(new Person("王兆雷"));

        persons.add(new Person("阿四"));
        persons.add(new Person("李洪磊"));


        //排序
        Collections.sort(persons, new Comparator<Person>() {
            @Override
            public int compare(Person lhs, Person rhs) {
                return lhs.getPinyin().compareTo(rhs.getPinyin());  //根据拼音排序
            }
        });
    }
}