Android获取手机联系人匹配用户表并按字母A-Z排序展示

1、前言

最近在做公司项目的时候遇到一个添加手机联系人的需求,主要有以下几个功能点:

  • 读取联系人:读取用户手机上的通讯录里的联系人列表
  • 好友排序:按照拼音顺序对好友进行排序,兼容英文数字符号等
  • 字母索引:右侧字母导航条,既可拖动也可点击,联动ListView滑动
  • 匹配:最后要将通讯录里的联系人列表与后台数据库里的用户表进行匹配

最终的大致效果如下:

手机联系人列表

特意写篇博客将整个实现过程记录下来,方便以后再次遇到这样的需求的时候可以直接使用CV大法,也希望能帮到刚好有这方面需求的朋友。

2、读取联系人

读取手机通讯录里的联系人主要是通过ContentResolver 来获取的,代码比较固定,直接贴代码:

先定义一个用来接收联系人的数据bean,主要是对id,name和phone三个字段进行赋值,其他字段主要是为了排序和匹配用户表用到的。

/**
 * @author hydCoder
 * @date 2017/10/11 10:50
 * @desc 手机联系人的数据bean
 * @email hyd_coder@163.com
 */

public class ContactInfo implements Comparable<ContactInfo> {
    public String id;
    public String name;
    public String phone;
    public String pinyin; // 姓名对应的拼音
    public String firstLetter; // 拼音的首字母
    public String userAvatar;
    public String userName;
    public String userNick;
    public int    isFriend;
    public String userId;
    public int gradeLevel;
    public String userPosition;
    public String userCompany;
    public int userType;
    public boolean isUser = false;

    public ContactInfo(String id, String name, String phone) {
        this.id = id;
        this.name = name;
        this.phone = phone;
        pinyin = Cn2Spell.getPinYin(name); // 根据姓名获取拼音
        firstLetter = pinyin.substring(0, 1).toUpperCase(); // 获取拼音首字母并转成大写
        if (!firstLetter.matches("[A-Z]")) { // 如果不在A-Z中则默认为“#”
            firstLetter = "#";
        }
    }

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

获取联系人数据的工具类:

    /**
 * @author hydCoder
 * @date 2017/10/11 10:53
 * @desc 获取手机联系人数据
 * @email hyd_coder@163.com
 */

public class ContactUtils {
    /**
     * 获取联系人数据
     *
     * @param context
     * @return
     */
    public static List<ContactInfo> getAllContacts(Context context) {
        List<ContactInfo> list = new ArrayList<>();
        // 获取解析者
        ContentResolver resolver = context.getContentResolver();
        // 访问地址
        Uri raw_contacts = Uri.parse("content://com.android.contacts/raw_contacts");
        Uri data = Uri.parse("content://com.android.contacts/data");
        // 查询语句
        // select contact_id from raw_contacts;//1 2 3 4
        // select mimetype,data1 from view_data where raw_contact_id=3;
        // Cursor cursor=resolver.query(访问地址, 返回字段 null代表全部, where 语句, 参数, 排序)
        Cursor cursor = resolver.query(raw_contacts, new String[] { "contact_id" }, null, null, null);

        while (cursor.moveToNext()) {
            // getColumnIndex根据名称查列号
            String id = cursor.getString(cursor.getColumnIndex("contact_id"));
            // 创建实例
            String name = "";
            String phone = "";
            Cursor item = resolver.query(data, new String[] { "mimetype", "data1" }, "raw_contact_id=?", new String[] { id }, null);

            while (item.moveToNext()) {
                String mimetype = item.getString(item.getColumnIndex("mimetype"));
                String data1 = item.getString(item.getColumnIndex("data1"));
                if ("vnd.android.cursor.item/name".equals(mimetype)) {
                    name = data1;
                } else if ("vnd.android.cursor.item/phone_v2".equals(mimetype)) {
                    // 有的手机号中间会带有空格
                    phone = data1.replace(" ","");
                }
            }
            ContactInfo info = new ContactInfo(id,name,phone);
            item.close();
            // 添加集合
            list.add(info);
        }

        cursor.close();
        return list;
    }
}

3、好友排序和字母索引

3.1、右侧字母索引的导航条--SideBar

这个可以在网上找到很多类似的,你也可以找一个自己喜欢的甚至自己写一个出来,我在项目里用的是这个

https://github.com/AlexLiuSheng/AnimSideBar

我把他的SideBar.java拷贝到项目里,修改了部分代码:

    public class SideBar extends TextView {
    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 = 6;
    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(getCurrentTextColor());
        textPaint.setTextSize(getTextSize());
        textPaint.setTextAlign(Paint.Align.CENTER);
        bigTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        bigTextPaint.setColor(getCurrentTextColor());
        bigTextPaint.setTextSize(getTextSize() * (scaleSize + 3));
        bigTextPaint.setTextAlign(Paint.Align.CENTER);
        scaleTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        scaleTextPaint.setColor(getCurrentTextColor());
        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;
                float nextItemY = itemH * (i + 1);
                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, bigTextPaint);
                }
            }
        }
        drawLetters(y, currentSelectIndex);
    }

    private void drawLetters(float y, int index) {
        //第一次进来没有缩放情况,默认画原图
        if (index == -1) {
            w = getMeasuredWidth();
            h = getMeasuredHeight();
            itemH = h / letters.length;
            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
            singleTextH = fontMetrics.descent - fontMetrics.ascent;
            for (int i = 0; i < letters.length; i++) {
                canvas.drawText(letters[i], w - getPaddingRight(), singleTextH + itemH * i, textPaint);
            }
            //触摸的时候画缩放图
        } else {
            //遍历所有字母
            for (int i = 0; i < letters.length; i++) {
                //要画的字母的起始Y坐标
                float currentItemToDrawY = singleTextH + itemH * i;
                float centerItemToDrawY;
                if (index < i)
                    centerItemToDrawY = singleTextH + itemH * (index + scaleItemCount);
                else
                    centerItemToDrawY = singleTextH + itemH * (index - scaleItemCount);
                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, textPaint);
                else
                    canvas.drawText(letters[i], drawX, singleTextH + itemH * i, scaleTextPaint);
            }
        }
    }

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

}

其中3个自定义的属性如下:

<?xml version="1.0" encoding="utf-8"?>  
<resources>  
    <declare-styleable name="SideBar">  
        <attr name="scaleSize" format="integer"/>  
        <attr name="scaleItemCount" format="integer"/>  
        <attr name="scaleWidth" format="dimension"/>  
    </declare-styleable>  
</resources>  

3.2、汉字转拼音工具类

比较尴尬的是,java中是没有提供接口和方法让我们直接将汉字转成拼音的。所以我们只能自己想办法了,一般有以下两种办法:

**1、 使用第三方pinyin4j的jar包 ** 下载地址
Android Studio也可直接依赖

compile 'com.belerweb:pinyin4j:2.5.0'

优点:使用简单,实用性好
缺点:需要依赖第三方jar包

2、 使用ASCII码和拼音的映射

优点:零依赖,只有一个Class,使用简单
缺点:只支持常见的一级汉字,对于一些不常见的汉字(亳bo)则无法正确获取拼音
(中文编码中一级汉字是按拼音排序的,容易映射。而二级汉字是按笔画部首排序的)

其实,无论是使用哪种方法,我发现都是没有去处理多音字的情况(毕竟这个真不好写),不过基本需求都可以满足啦。

我在这里直接使用的就是pinyin4j的jar包。但我还是基于pinyin4j写了个转换的工具类:

    /** 
 * 汉字转换位汉语拼音,英文字符不变 
 */  
public class Cn2Spell {  
  
    public static StringBuffer sb = new StringBuffer();  
  
    /** 
     * 获取汉字字符串的首字母,英文字符不变 
     * 例如:阿飞→af 
     */  
    public static String getPinYinHeadChar(String chines) {  
        sb.setLength(0);  
        char[] chars = chines.toCharArray();  
        HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();  
        defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);  
        defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);  
        for (int i = 0; i < chars.length; i++) {  
            if (chars[i] > 128) {  
                try {  
                    sb.append(PinyinHelper.toHanyuPinyinStringArray(chars[i], defaultFormat)[0].charAt(0));  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            } else {  
                sb.append(chars[i]);  
            }  
        }  
        return sb.toString();  
    }  
  
    /** 
     * 获取汉字字符串的第一个字母 
     */  
    public static String getPinYinFirstLetter(String str) {  
        sb.setLength(0);  
        char c = str.charAt(0);  
        String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c);  
        if (pinyinArray != null) {  
            sb.append(pinyinArray[0].charAt(0));  
        } else {  
            sb.append(c);  
        }  
        return sb.toString();  
    }  
  
    /** 
     * 获取汉字字符串的汉语拼音,英文字符不变 
     */  
    public static String getPinYin(String chines) {  
        sb.setLength(0);  
        char[] nameChar = chines.toCharArray();  
        HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();  
        defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);  
        defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);  
        for (int i = 0; i < nameChar.length; i++) {  
            if (nameChar[i] > 128) {  
                try {  
                    sb.append(PinyinHelper.toHanyuPinyinStringArray(nameChar[i], defaultFormat)[0]);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            } else {  
                sb.append(nameChar[i]);  
            }  
        }  
        return sb.toString();  
    }  
  
}  

3.3、让联系人可以根据拼音来排序

代码在上面已经贴过了,这里就不重复贴了,其实就是让JavaBean实现comparable接口,并重写comparaTo方法。再在comparaTo方法里根据首字母判断,首字母为“#”都放在最后,都为“#”或者都是字母时才根据拼音来比较排序。

4、与后台数据库进行匹配

我与我们后台的开发大佬商量好的是将获取到的联系人集合里的手机号用","连接起来传给他,他再将匹配到的联系人数据返回给我,我再进行数据整合并处理交互逻辑。代码比较简单,这里就不贴了。

5、组装

由于没有把这个单独抽出来,所以只能贴部分关键代码。。。。。。

Activity的布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="@color/gray_e5e5e5">

    <com.sdalolo.genius.utils.CustomNavigatorBar
        android:id="@+id/cn_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/title_bar_size"
        android:paddingRight="10dp"
        android:layout_alignParentTop="true"
        app:leftImage="@drawable/backtrack"
        app:leftImageVisiable="true"
        app:leftTextVisibale="false"
        app:midText="添加手机联系人"
        app:midTextFontSize="@dimen/title_size"
        app:midTextFontColor="@color/auxiliary_color"
        app:rightTextVisible="false"
        app:rightImageVisible="false"
        app:titleBarBackground="@color/main_color">
    </com.sdalolo.genius.utils.CustomNavigatorBar>

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/cn_bar"
        android:divider="@color/divide_color"
        android:scrollbars="none"
        android:dividerHeight="1dp">
    </ListView>

    <com.sdalolo.genius.ui.view.SideBar
        android:id="@+id/side_bar"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/cn_bar"
        android:layout_alignParentRight="true"
        android:paddingRight="10dp"
        android:textColor="@color/auxiliary_color"
        android:textSize="14sp" />

</RelativeLayout>

activity中的部分代码:

mSideBar.setOnStrSelectCallBack(new SideBar.ISideBarSelectCallBack() {
        @Override
        public void onSelectStr(int index, String selectStr) {
            if (mAllContacts != null) {
                for (int i = 0; i < mAllContacts.size(); i++) {
                    if (selectStr.equalsIgnoreCase(mAllContacts.get(i).firstLetter)) {
                        mListView.setSelection(i); // 选择到首字母出现的位置
                        return;
                    }
                }
            }
        }
    });

adapter中的部分代码:

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

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

6、结语

到这里基本上效果就实现了,是不是很简单,不过如果编译版本大于23的话,记得动态申请Manifest.permission.READ_CONTACTS权限,不然会Crash,别问我怎么知道的。。。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,072评论 25 707
  • 记得在我刚开始接触到美团网的时候就对美团网这个城市定位、选择城市功能很感兴趣,觉得它做得很棒。有如下几个点:一:实...
    Angelicas阅读 9,617评论 1 33
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 《父亲》 文/白传英 有时候觉得自己像小溪 碧波荡漾一年四季 当我干涸的时候 父亲就像那黄河 有时候觉得自己...
    白清风阅读 142评论 0 0
  • 下了一天的雨 抽了个空,和同行的姐姐去了咖啡馆, 可是想去的那家啊没有营业,后发现了大鱼很安静的大鱼,美的低调又张...
    乔未9574阅读 152评论 0 0