先看一下效果图吧!

前端实现通讯录类似的字母点击索引 通讯录字母索引调整_索引


我们需求就是在右侧显示字母和“#”,只按顺序显示所列名字拼音的首字母,没使用的不显示,若有名字拼音不是“A-Z”开头的,显示“#”。

如果需求是显示全部26个字母和“#”,或需要触摸显示放大字母,可自行修改。

需要用到的jar包:pinyin4j-2.5.0.jar

还需要用到一个工具类:

package 包名.utils;

import android.annotation.SuppressLint;
import android.text.TextUtils;
import android.util.Log;

import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;

public class PinYin {

    /**
     * 讲汉字转换为拼音
     *
     * @param src
     * @return
     */
    @SuppressLint("DefaultLocale")
    public static String getPinYin(String src) {
        char[] charArr = null;
        if(TextUtils.isEmpty(src)){
            return "#";
        }
        charArr = src.toCharArray();
        String[] strArr = new String[charArr.length];
        // 设置汉字拼音输出的格式
        HanyuPinyinOutputFormat hpOpf = new HanyuPinyinOutputFormat();
        hpOpf.setCaseType(HanyuPinyinCaseType.LOWERCASE);
        hpOpf.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        hpOpf.setVCharType(HanyuPinyinVCharType.WITH_V);
        String str = "";
        int t0 = charArr.length;
        try {
            for (int i = 0; i < t0; i++) {
                // 判断是否为汉字字符
                if (Character.toString(charArr[i])
                        .matches("[\\u4E00-\\u9FA5]+")) {
                    strArr = PinyinHelper.toHanyuPinyinStringArray(charArr[i],
                            hpOpf);// 将汉字的几种全拼都存到t2数组中
                    str += strArr[0];// 取出该汉字全拼的第一种读音并连接到字符串t4后
                } else {
                    // 如果不是汉字字符,直接取出字符并连接到字符串t4后
                    str += Character.toString(charArr[i]);
                }
            }
        } catch (BadHanyuPinyinOutputFormatCombination e) {
            Log.e("getPinYin error.", e.toString());
        }
        return str;
    }

    /**
     * 将汉字转换为拼音--首字母
     *
     * @param str
     * @return
     */
    public static String getPinYinHeadChar(String str) {
        String convert = "";
        if(TextUtils.isEmpty(str)){
            return "#";
        }
        for (int j = 0; j < str.length(); j++) {
            char word = str.charAt(j);
            // 提取汉字的首字母
            String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(word);
            if (pinyinArray != null) {
                convert += pinyinArray[0].charAt(0);
            } else {
                convert += word;
            }
        }
        return convert;
    }

    /**
     * 讲汉字转换为字节序列
     *
     * @param cnStr
     * @return
     */
    public static String getCnASCII(String cnStr) {
        StringBuffer strBuf = new StringBuffer();
        // 将字符串转换成字节序列
        byte[] bGBK = cnStr.getBytes();
        for (int i = 0; i < bGBK.length; i++) {
            strBuf.append(Integer.toHexString(bGBK[i] & 0xff));
        }
        return strBuf.toString();
    }

}

然后是自定义索引的控件:

package 包名.view;

/**
 * Created by qby on 2017/2/20 0020.
 */

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.MotionEvent;

import 包名.R;

public class SideBar extends AppCompatTextView {
    private String[] letters = new 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 Paint textPaint;
//    private Paint bigTextPaint;
//    private Paint scaleTextPaint;

    private Canvas canvas;
    private int itemH;
    private int w;
    private int h;
    /**
     * 普通情况下字体大小
     */
    float singleTextH;
    /**
     * 缩放离原始的宽度
     */
    private float scaleWidth;
    /**
     * 滑动的Y
     */
    private float eventY = 0;
    /**
     * 缩放的倍数
     */
    private int scaleSize = 1;
    /**
     * 缩放个数item,即开口大小
     */
    private int scaleItemCount = 0;
    private ISideBarSelectCallBack callBack;

    public SideBar(Context context) {
        this(context, null);
    }

    public SideBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SideBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.SideBar);
            scaleSize = ta.getInteger(R.styleable.SideBar_scaleSize, 1);
            scaleItemCount = ta.getInteger(R.styleable.SideBar_scaleItemCount, 6);
            scaleWidth = ta.getDimensionPixelSize(R.styleable.SideBar_scaleWidth, dp(100));
            ta.recycle();
        }
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(Color.GRAY);
        textPaint.setTextSize(getTextSize());
        textPaint.setTextAlign(Paint.Align.CENTER);
//        bigTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//        bigTextPaint.setColor(Color.GREEN);
//        bigTextPaint.setTextSize(getTextSize() * (scaleSize + 3));
//        bigTextPaint.setTextAlign(Paint.Align.CENTER);
//        scaleTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//        scaleTextPaint.setColor(Color.GRAY);
//        scaleTextPaint.setTextSize(getTextSize() * (scaleSize + 1));
//        scaleTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    public void setDataResource(String[] data) {
        letters = data;
        invalidate();
    }

    public void setOnStrSelectCallBack(ISideBarSelectCallBack callBack) {
        this.callBack = callBack;
    }

    /**
     * 设置字体缩放比例
     *
     * @param scale
     */
    public void setScaleSize(int scale) {
        scaleSize = scale;
        invalidate();
    }

    /**
     * 设置缩放字体的个数,即开口大小
     *
     * @param scaleItemCount
     */
    public void setScaleItemCount(int scaleItemCount) {
        this.scaleItemCount = scaleItemCount;
        invalidate();
    }

    private int dp(int px) {
        final float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (px * scale + 0.5f);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                if (event.getX() > (w - getPaddingRight() - singleTextH - 10)) {
                    eventY = event.getY();
                    invalidate();
                    return true;
                } else {
                    eventY = 0;
                    invalidate();
                    break;
                }
            case MotionEvent.ACTION_CANCEL:
                eventY = 0;
                invalidate();
                return true;
            case MotionEvent.ACTION_UP:
                if (event.getX() > (w - getPaddingRight() - singleTextH - 10)) {
                    eventY = 0;
                    invalidate();
                    return true;
                } else
                    break;
        }
        return super.onTouchEvent(event);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        this.canvas = canvas;

        DrawView(eventY);
    }

    private void DrawView(float y) {
        int currentSelectIndex = -1;
        if (y != 0) {
            for (int i = 0; i < letters.length; i++) {
                float currentItemY = itemH * i + itemH * (27 - letters.length)/2;
                float nextItemY = itemH * (i + 1) + itemH * (27 - letters.length)/2;
                if (y >= currentItemY && y < nextItemY) {
                    currentSelectIndex = i;
                    if (callBack != null) {
                        callBack.onSelectStr(currentSelectIndex, letters[i]);
                    }
                    //画大的字母
//                    Paint.FontMetrics fontMetrics = bigTextPaint.getFontMetrics();
//                    float bigTextSize = fontMetrics.descent - fontMetrics.ascent;
//                    canvas.drawText(letters[i], w - getPaddingRight() - scaleWidth - bigTextSize, singleTextH + itemH * i + itemH * (27 - letters.length)/2, bigTextPaint);
                }
            }
        }
        drawLetters(y, currentSelectIndex);
    }

    private void drawLetters(float y, int index) {
        //第一次进来没有缩放情况,默认画原图
        if (index == -1) {
            w = getMeasuredWidth();
            h = getMeasuredHeight();
            itemH = h / 27;
            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
            singleTextH = fontMetrics.descent - fontMetrics.ascent;
            for (int i = 0; i < letters.length; i++) {
                canvas.drawText(letters[i], w - getPaddingEnd(), singleTextH + itemH * i  + itemH * (27 - letters.length)/2, textPaint);
            }
            //触摸的时候画缩放图
        } else {
            //遍历所有字母
            for (int i = 0; i < letters.length; i++) {
                //要画的字母的起始Y坐标
                float currentItemToDrawY = singleTextH + itemH * i  + itemH * (27 - letters.length)/2;
                float centerItemToDrawY;
                if (index < i)
                    centerItemToDrawY = singleTextH + itemH * (index + scaleItemCount) + itemH * (27 - letters.length)/2;
                else
                    centerItemToDrawY = singleTextH + itemH * (index - scaleItemCount) + itemH * (27 - letters.length)/2;
                float delta = 1 - Math.abs((y - currentItemToDrawY) / (centerItemToDrawY - currentItemToDrawY));
                float maxRightX = w - getPaddingRight();
                //如果大于0,表明在y坐标上方
//                scaleTextPaint.setTextSize(getTextSize() + getTextSize() * delta);
                float drawX = maxRightX - scaleWidth * delta;
                //超出边界直接花在边界上
//                if (drawX > maxRightX)
                    canvas.drawText(letters[i], maxRightX, singleTextH + itemH * i  + itemH * (27 - letters.length)/2, textPaint);
//                else
//                    canvas.drawText(letters[i], drawX, singleTextH + itemH * i  + itemH * (27 - letters.length)/2, scaleTextPaint);
            }
        }
    }

    public interface ISideBarSelectCallBack {
        void onSelectStr(int index, String selectStr);
    }

}

准备完毕,使用SideBar时只需要在布局中加入该控件:

<RelativeLayout
        android:id="@+id/rl"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="10dp"
            android:layout_marginTop="15dp" />

        <包名.view.SideBar
            android:id="@+id/sideBar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"
            android:paddingEnd="10dp" />
    </RelativeLayout>

想要让字母按A-Z#排序,需要对获取的从后台获取的JavaBean实现Comparable:

package 包名.bean;

import 包名.utils.PinYin;

import java.util.List;

/**
 * Created by qby on 2016/8/12 0012.
 */

public class FansListBean {

    public int status;
    public String message;
    /**
     * id : 7
     * user_name : j
     * phone : 120
     * user_type : 1
     * contact_user_id : 1
     * user_id : 27
     * avator : null
     */

    public List<DataBean> data;

    public static class DataBean implements Comparable<DataBean>{
        private String name; // 姓名
        private String pinyin; // 姓名对应的拼音
        private String firstLetter; // 拼音的首字母

        public int id;
        public String user_name;
        public int user_type;
        public int contact_user_id;
        public String avator;
        public DataBean(){

        }
        public DataBean(int id,String name,int user_type,int contact_user_id,String avator) {
            this.id = id;
            this.name = name;
            this.user_type = user_type;
            this.contact_user_id = contact_user_id;
            this.avator = avator;
            pinyin = PinYin.getPinYin(name);
            firstLetter = String.valueOf(PinYin.getPinYinHeadChar(name).charAt(0)).toUpperCase(); // 获取拼音首字母并转成大写
            if (!firstLetter.matches("[A-Z]")) { // 如果不在A-Z中则默认为“#”
                firstLetter = "#";
            }
        }

        public String getName() {
            return name;
        }

        public String getPinyin() {
            return pinyin;
        }

        public String getFirstLetter() {
            return firstLetter;
        }

        @Override
        public int compareTo(DataBean another) {
            if (firstLetter.equals("#") && !another.getFirstLetter().equals("#")) {
                return 1;
            } else if (!firstLetter.equals("#") && another.getFirstLetter().equals("#")){
                return -1;
            } else {
                return pinyin.compareToIgnoreCase(another.getPinyin());
            }
        }
    }
}

列表条目布局: lv_contacts_item

<?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:background="@color/white"
    android:orientation="vertical">

    <LinearLayout
        android:id="@+id/ll_tip"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/wheel_white"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_tip"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dp"
            android:layout_marginEnd="25dp"
            android:layout_marginStart="25dp"
            android:layout_marginTop="5dp"
            android:text="A"
            android:textColor="#888"
            android:textSize="14dp" />

        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:background="@color/lightwhite" />
    </LinearLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="10dp"
        android:paddingEnd="15dp"
        android:paddingStart="15dp"
        android:paddingTop="10dp"
        >

        <com.duola.duolainter.duolainter.view.CircleImageView
            android:id="@+id/iv_user"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_centerVertical="true"
            android:layout_marginStart="5dp"
            android:src="@mipmap/icon_user_photo_default" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginStart="10dp"
            android:layout_toEndOf="@+id/iv_user"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tv_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="阿拉蕾"
                android:textColor="@color/black"
                android:textSize="16dp" />

            <TextView
                android:id="@+id/tv_user_type"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="5dp"
                android:text="车主" />

        </LinearLayout>
    </RelativeLayout>

</LinearLayout>

下面就是在页面中的使用了:

private ArrayList<FansListBean.DataBean> datas = new ArrayList<FansListBean.DataBean>();//声明一个集合
private ArrayAdapter<FansListBean.DataBean> adapter;//声明适配器
声明控件:
private SideBar sideBar;
初始化控件:
sideBar = (SideBar) findViewById(R.id.sideBar);
sideBar.setOnStrSelectCallBack(new SideBar.ISideBarSelectCallBack() {
            @Override
            public void onSelectStr(int index, String selectStr) {
                for (int i = 0; i < datas.size(); i++) {
                    if (selectStr.equalsIgnoreCase(datas.get(i).getFirstLetter())) {
                        listView.setSelection(i); // 选择到首字母出现的位置
                        return;
                    }
                }

            }
        });

设置adapter:
adapter = new ArrayAdapter<FansListBean.DataBean>(ctx, R.layout.lv_contacts_item, datas) {

            /**
             * 获取catalog首次出现位置
             */
            public int getPositionForSection(String catalog) {
                for (int i = 0; i < getCount(); i++) {
                    String sortStr = datas.get(i).getFirstLetter();
                    if (catalog.equalsIgnoreCase(sortStr)) {
                        return i;
                    }
                }
                return -1;
            }

            @Override
            public View getView(final int position, View convertView, ViewGroup parent) {
                ViewHolder holder;
                if (convertView == null) {
                    convertView = View.inflate(ctx, R.layout.lv_contacts_item, null);
                    holder = new ViewHolder(convertView);
                    convertView.setTag(holder);
                } else {
                    holder = (ViewHolder) convertView.getTag();
                }
                FansListBean.DataBean dataBean = datas.get(position);

                //根据position获取首字母作为目录catalog
                String catalog = datas.get(position).getFirstLetter();

                //如果当前位置等于该分类首字母的Char的位置 ,则认为是第一次出现
                if (position == getPositionForSection(catalog)) {
                    holder.ll_tip.setVisibility(View.VISIBLE);
                    holder.tv_tip.setText(catalog.toUpperCase());
                } else {
                    holder.ll_tip.setVisibility(View.GONE);
                }

                if (!TextUtils.isEmpty(dataBean.avator)) {//设置网络图片
                   Picasso.with(ctx).load(dataBean.avator).error(R.mipmap.icon_user_photo_default).resize(Tool.dip2px(ctx, 50), Tool.dip2px(ctx, 50)).into(holder.iv_user);
                } else {//设置默认图片
                    Picasso.with(ctx).load(R.mipmap.icon_user_photo_default).resize(Tool.dip2px(ctx, 50), Tool.dip2px(ctx, 50)).into(holder.iv_user);
                }

                if (dataBean.user_type == 0) {
                    holder.tv_user_type.setText("货主");
                } else if (dataBean.user_type == 1) {
                    holder.tv_user_type.setText("车主");
                }
                holder.tv_name.setText(dataBean.getName());
                return convertView;
            }
        };
        listView.setAdapter(adapter);
请求回调中:
FansListBean fansListBean = new Gson().fromJson(s, FansListBean.class);

int status = fansListBean.status;
if (status == 1) {
   datas.clear();
   List<FansListBean.DataBean> data = fansListBean.data;
   if (data.size() == 0) {
       rl.setVisibility(View.GONE);
   } else {
       rl.setVisibility(View.VISIBLE);

       for (FansListBean.DataBean bean : data) {
           if (!TextUtils.isEmpty(bean.user_name)) {
               datas.add(new FansListBean.DataBean(bean.id, bean.user_name, bean.user_type, bean.contact_user_id, bean.avator));
           }

       }
       Collections.sort(datas); // 对list进行排序,需要让User实现Comparable接口重写compareTo方法
       adapter.notifyDataSetChanged();
       ArrayList<String> al = new ArrayList<String>();
       for (FansListBean.DataBean bean : datas) {
           if (!al.contains(bean.getFirstLetter())) {
               al.add(bean.getFirstLetter().toUpperCase());
           }
       }
       indexs = al.toArray(new String[al.size()]);
       sideBar.setDataResource(indexs);//把要显示的字母传给SideBar
   }

} else {
   ToastUtil.showTextToast(ctx, fansListBean.message);
}