最近因为项目需要,研究了Android联系人相关内容,包括联系人数据库,获取联系人数据,使用ListView展示联系人。我将按照以下几点记录:
  
  Android存储联系人数据库表结构
  获取联系人数据
  联系人列表效果

  一Android存储联系人数据库表结构
  要想搞清楚Android联系人内容,首先就得清楚这些内容在Android中是怎么存储的,为了搞清楚这个问题,可以直接取出Android中存储联系人的数据库contact2.db。它的存储路径:
  /data/data/com.android.providers.contacts/databases.contacts2.db
  如果是模拟器,可以直接取出来。如果是真机的话需要获取root权限。取出之后用可以查看SQLite数据库的软件(我用的是SQLiteSpy)打开,结构如下:
仿Android6.0联系人列表_第1张图片

  这里包括了很多表,而我们只看几个比较重要的表,contacts,data,mimetypes,raw_contacts。

  contacts表
  该表保存了所有的手机联系人,包括每个联系人id、联系次数(times_contacted)、最后一次联系的时间(last_time_contactde)、是否含有电话号码(has_phone_number)、是否被收藏(starred)等信息。来看看以下字段:
  _id :表id,主要用于其他表通过该字段来查找相应的数据。
  lookup:该字段是不会改变的,联系人的信息可能改变,但是该字段是一直存储不会改变的。
  name_raw_contact_id:这个字段对于我们暂时没有什么作用,先忽略他。
  仿Android6.0联系人列表_第2张图片

   raw_contacts表
   该表中的字段比较多,也包含了大量的信息,对于我们实现联系人列表有很大的作用,这里我只调几个字段来看看,其他的可以自己查看:
   _id:其他表如contacts,data表可以通过raw_contact_id来查找raw_contacts表中的数据。
   contact_id:通过该字段raw_contacts表就可以去查找contacts表,这样两张表就联系起来了。
   display_name(display_name_alt):联系人姓名,不用多说。
   sort_key:我们在取数据时可以安装该字段来排序。这样我们取到的数据就是排好顺序的。
   phone_book_label:联系人首字母,我们可以取得该字段来实现类似微信通讯录联系人的效果。
   deleted:该联系人是否被删除。
   

  mimetypes表
  这个表很有意思也很重要,他主要是定义了联系人的数据类型,如果我们想自己定义一个联系人属性需要在这个表中添加,例如:我想扩展添加一个微信号,我们可以这样vnd.android.cursor.item/weichat,可以存储微信号等属性,有兴趣的可以去自己实现一下。
  

  data表
  该表保存了所有创建过的手机联系人的信息保存在列data1至data15中,该表保存了两个ID:mimetype_id和raw_contact_id,从而将data表和
raw_contacts表,mimetypes表联系起来了。在看看mimetype_id这个字段代表是什么意思呢,看上图就明白了,他其实代表的是该行的数据类型,下图中第一行mimetype_id 为1,那么对应到mimetypes表中是vnd.android.cursor.item/phone_v2,表示改行数据是电话号码。接着看data1就是号码,data2是号码类型(住宅,手机,座机等等)。改表也是可以扩展的。
仿Android6.0联系人列表_第3张图片

  以上几个表都是比较重要的,从这几个表中我们基本上可以满足我们做联系人列表的需求了。这里还要说一下,可能细心一点的话,会发觉有些不同表中有同一字段,这是不是违背了数据库设计规范造成数据冗余呢,Google不可能想不到这个问题,这样做主要是为了用成本较低的存储空间,去换取在表中联合查询时减少的时间。用空间换时间是不错的选择。
  到这儿,已经对联系人数据库有了初步的认识,接下来我们就来看看怎么获取这些表中的数据。
  
  获取联系人数据
  
  Android4.0之后在android.provider包下有一个ContactsContract类,用来管理联系人信息。该类结构比较复杂,有三个比较重要的内部类ConstractContact.Data,ConstractContact.RawContacts,ConstractContact.Contacts。这三个类实际上就是对应着上边介绍的data,raw_contacts,contacts三张表。
  在ConstractContact.Phone中有个 Phone.CONTENT_URI字段,看源码可以知道他指向的是“content:// com.android.contacts/data/phones”,而这个url实际上对应这data,contacts,raw_contacts,这三个表。再一次证明我们的联系人数据就是从三张表取的。
  来看看具体代码,我们取出联系人的姓名和电话号码,并安装首字母排序
  

private static final String PHONE_BOOK_LABLE="phonebook_label";    /**需要查询的字段**/    private static final String[]PHONES_PROJECTION={Phone.DISPLAY_NAME            ,Phone.NUMBER,PHONE_BOOK_LABLE};    /**联系人显示名称**/    private static final int PHONES_DISPLAY_NAME_INDEX = 0;    /**电话号码**/    private static final int PHONES_NUMBER_INDEX = 1; ContentResolver mResolver=getContentResolver();                    //查询联系人数据,query的参数Phone.SORT_KEY_PRIMARY表示将结果集按Phone.SORT_KEY_PRIMARY排序                    Cursor cursor=mResolver.query(Phone.CONTENT_URI                            ,PHONES_PROJECTION,null,null,Phone.SORT_KEY_PRIMARY);                    if(cursor!=null){                        while (cursor.moveToNext()){                            ContactsModel model=new ContactsModel();                            model.setPhone(cursor.getString(PHONES_NUMBER_INDEX));                            if(TextUtils.isEmpty(model.getPhone())){                                continue;                            }                            model.setName(cursor.getString(PHONES_DISPLAY_NAME_INDEX));                            model.setPhonebook_label(cursor.getString(cursor.getColumnIndex(PHONE_BOOK_LABLE)));                            contactsModelList.add(model);                        }                        cursor.close();                    }

  来看看以下字段phonebook_label,就是在raw_contact表中,存储的是首字母。我可以用他来作为分组的label

private static final String PHONE_BOOK_LABLE="phonebook_label";

  该数组是我们需要查询字段的集合,需要查询什么字段,直接在该数组中添加就ok了。
  

/**需要查询的字段**/    private static final String[]PHONES_PROJECTION={Phone.DISPLAY_NAME            ,Phone.NUMBER,PHONE_BOOK_LABLE};

  为了方便,我直接在取数据时就以Phone.SORT_KEY_PRIMARY(实际上就是“sort_key”字段)排好序了。这样我们就取得了联系人数据了。这里我们定义一下联系人model:
  

/** * Created by Administrator on 2015/11/23. */public class ContactsModel {    private String name;    private String phone;    private String phonebook_label;    public String getPhonebook_label() {        return phonebook_label;    }    public void setPhonebook_label(String phonebook_label) {        this.phonebook_label = phonebook_label;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getPhone() {        return phone;    }    public void setPhone(String phone) {        this.phone = phone;    }}

  还有一个非常重要的就是需要添加读取联系人权限(如果需要增删改查还需要写权限)。在清单文件AndroidManifest.xml中添加:
 

<uses-permission android:name="android.permission.READ_CONTACTS"/>

  PS:如果targetSDK>=23,需要动态获取权限。可以参考一下:
  Stack overflow
  在获取到数据源之后,我们就需要将其加载到ListView中了。

  联系人列表效果
类似的效果有很多种实现的方法,我是借鉴的比较常用的SideBar来作为右侧的字母导航,ListView部分通过监听AbsListView.OnScrollListener来实现滑动和停靠。

整个布局:

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" 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:divider="@null" android:background="#FFFFFF">    </ListView>    <com.chuck.contactsdemo.SideBar  android:id="@+id/sideBar" android:layout_width="20dp" android:layout_height="match_parent" android:layout_gravity="right" />        <!--左上角用于停靠-->    <TextView  android:id="@+id/index" android:layout_width="45dp" android:layout_height="45dp" android:layout_gravity="top|left" android:textSize="16sp" android:gravity="center" android:background="#FFFFFF"/>        <!--右边跟随触摸位置移动-->    <TextView  android:id="@+id/tv_toast" android:layout_width="60dp" android:layout_height="60dp" android:textSize="16sp" android:gravity="center" android:background="@color/colorPrimary" android:layout_marginRight="20dp" android:layout_gravity="right"/></FrameLayout>

   实现代码:
   首先是SideBar.class

package com.chuck.contactsdemo;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Typeface;import android.graphics.drawable.ColorDrawable;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.TextView;import com.nineoldandroids.animation.ObjectAnimator;/** * 通讯录右边导航条 */public class SideBar extends View {    /** * 需要展示的导航内容 */    public static String[] contentArray = { "↑", "☆", "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 OnTouchTextChangeListener onTouchTextChangeListener;// 触摸位置监听器    private Paint mPaint = new Paint();//画笔对象    private int choosePosition = -1;//选中位置    private Context context;    private TextView mToastTextView;//选择某一项时弹出的TextView    public SideBar(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        this.context = context;    }    public SideBar(Context context, AttributeSet attrs) {        this(context, attrs,0);    }    public SideBar(Context context) {        this(context,null);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        // 获取焦点改变背景颜色.         int height = getHeight();// 获取视图高度         int width = getWidth(); // 获取视图宽度         int singleHeight = height / contentArray.length;// 获取每一个字母的高度         for (int i = 0; i < contentArray.length; i++) {              mPaint.setColor(Color.rgb(86, 86, 86));              // paint.setColor(Color.WHITE);             mPaint.setTypeface(Typeface.DEFAULT_BOLD);              mPaint.setAntiAlias(true);              mPaint.setTextSize(CommonUtil.sp2px(context, 15));              // x坐标等于中间-字符串宽度的一半.             float xPos = width / 2 - mPaint.measureText(contentArray[i]) / 2;              float yPos = singleHeight * i + singleHeight;              canvas.drawText(contentArray[i], xPos, yPos, mPaint);              mPaint.reset();// 重置画笔         }      }    @Override    public boolean dispatchTouchEvent(MotionEvent event) {        final int action = event.getAction();          final float y = event.getY();// 点击y坐标         final int oldChoose = choosePosition;          final OnTouchTextChangeListener listener = onTouchTextChangeListener;          final int c = (int) (y / getHeight() * contentArray.length);// 点击y坐标所占总高度的比例*contentArray数组的长度就等于点击b中的个数.         switch (action) {          case MotionEvent.ACTION_DOWN:        case MotionEvent.ACTION_MOVE:            setBackgroundDrawable(new ColorDrawable(0xE6D6DAE1));            if (oldChoose != c) {                  if (c >= 0 && c < contentArray.length) {                      if (listener != null) {                          listener.onTouchTextChanged(contentArray[c]);                      }                      if (mToastTextView != null) {                          mToastTextView.setText(contentArray[c]);                          mToastTextView.setVisibility(View.VISIBLE);                        ObjectAnimator.ofFloat(mToastTextView, "translationY", y).start();                    }                      choosePosition = c;                    invalidate();                }              }            break;        case MotionEvent.ACTION_UP:              setBackgroundDrawable(new ColorDrawable(0x00000000));              choosePosition = -1;//             invalidate();              if (mToastTextView != null) {                  mToastTextView.setVisibility(View.GONE);              }              break;          default:              break;          }          return true;     }    public void setToastTextView(TextView tv){        mToastTextView = tv;    }    /** * 外部绑定触摸位置变化监听器方法 * * @param onTouchTextChangeListener */    public void setOnTouchTextChangeListener(            OnTouchTextChangeListener onTouchTextChangeListener) {        this.onTouchTextChangeListener = onTouchTextChangeListener;    }    /** * 触摸位置改变监听器 */    public interface OnTouchTextChangeListener {        /** * 触摸位置发生改变后的回调方法 * * @param s * 当前触摸的内容 */        void onTouchTextChanged(String s);    }}

接下来我们先定义一下接口 UpdateIndexUIListener用来监听ListView现在滑动位置,以便设置左上角View显示字母及移动距离

package com.chuck.contactsdemo.interfaces;/** * Created by Administrator on 2015/11/29. */public interface UpdateIndexUIListener {    public void onUpdatePosition(int position);    public void onUpdateText(String mtext);}

还有ListView的适配器:

package com.chuck.contactsdemo;import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.AbsListView;import android.widget.BaseAdapter;import android.widget.TextView;import com.chuck.contactsdemo.interfaces.UpdateIndexUIListener;import java.util.ArrayList;import java.util.List;/** * Created by Administrator on 2015/11/24. */public class ContactsListAdapter extends BaseAdapter implements AbsListView.OnScrollListener{    private Context mContext;    private List<ContactsModel> contactsModelList=new ArrayList<>();    private UpdateIndexUIListener listener;    private int mCurrentFirstPosition=0;    private int lastFirstPosition=-1;    public ContactsListAdapter(Context mContext,List<ContactsModel> contactsModelList) {        this.mContext = mContext;        this.contactsModelList=contactsModelList;    }    @Override    public int getCount() {        return contactsModelList.size();    }    @Override    public Object getItem(int position) {        return contactsModelList.get(position);    }    @Override    public long getItemId(int position) {        return position;    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {        ViewHolder mViewHolder;        if(convertView==null){            mViewHolder=new ViewHolder();            convertView= LayoutInflater.from(mContext).inflate(R.layout.item_contact_listview,null);            mViewHolder.tv_name= (TextView) convertView.findViewById(R.id.tv_name);            mViewHolder.tv_group_index= (TextView) convertView.findViewById(R.id.tv_group_index);            convertView.setTag(mViewHolder);        }else {            mViewHolder= (ViewHolder) convertView.getTag();        }        mViewHolder.tv_name.setText(contactsModelList.get(position).getName());        if(position>0&&!contactsModelList.get(position).getPhonebook_label().equals(contactsModelList.get(position-1).getPhonebook_label())){            mViewHolder.tv_group_index.setText(contactsModelList.get(position).getPhonebook_label());        }else if(position==0){            mViewHolder.tv_group_index.setText(contactsModelList.get(position).getPhonebook_label());        }else {            mViewHolder.tv_group_index.setText("");        }        return convertView;    }    @Override    public void onScrollStateChanged(AbsListView view, int scrollState) {    }    @Override    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {        mCurrentFirstPosition=firstVisibleItem;        if(listener!=null){            listener.onUpdateText(contactsModelList.get(mCurrentFirstPosition).getPhonebook_label());        }        if(firstVisibleItem!=lastFirstPosition){            if(listener!=null){                listener.onUpdatePosition(0);            }        }        if(lastFirstPosition!=-1&&!contactsModelList.get(firstVisibleItem).getPhonebook_label()                .equals(contactsModelList.get(firstVisibleItem+1).getPhonebook_label())){            View childView=view.getChildAt(0);            int bottom=childView.getBottom();            int height=childView.getHeight();            int distance=bottom-height;            if(distance<0){//如果新的section                listener.onUpdatePosition(distance);            }else {                listener.onUpdatePosition(0);            }        }        lastFirstPosition=firstVisibleItem;    }    public void setUpdateIndexUIListener(UpdateIndexUIListener listener){        this.listener=listener;    }    private class ViewHolder{        private TextView tv_name;        private TextView tv_group_index;    }}

这里adapter可以根据自己的需要实现,这里只是为了演示而写的。
  最后是Activity:
  

package com.chuck.contactsdemo;import android.Manifest;import android.animation.ObjectAnimator;import android.content.ContentResolver;import android.content.pm.PackageManager;import android.database.Cursor;import android.os.Build;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.provider.ContactsContract.CommonDataKinds.Phone;import android.support.v7.app.AppCompatActivity;import android.text.TextUtils;import android.view.View;import android.view.ViewGroup;import android.widget.ListView;import android.widget.TextView;import com.chuck.contactsdemo.interfaces.UpdateIndexUIListener;import java.util.ArrayList;import java.util.List;import java.util.regex.Pattern;/** * Created by Administrator on 2015/11/23. */public class ContactsListActivity extends AppCompatActivity implements UpdateIndexUIListener ,SideBar.OnTouchTextChangeListener{    private static final int PERMISSIONS_REQUEST_CODE_ACCESS_READ_CONTACTS=0x11;    private static final String PHONE_BOOK_LABLE="phonebook_label";    /**需要查询的字段**/    private static final String[]PHONES_PROJECTION={Phone.DISPLAY_NAME            ,Phone.NUMBER,PHONE_BOOK_LABLE};    /**联系人显示名称**/    private static final int PHONES_DISPLAY_NAME_INDEX = 0;    /**电话号码**/    private static final int PHONES_NUMBER_INDEX = 1;    private ListView listView;    private SideBar sideBar;    private TextView tv_toast;    private TextView tv_index;    private ContactsListAdapter mAdapter;    private List<ContactsModel> contactsModelList=new ArrayList<>();    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_contactlist);        initViews();        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&                checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED){            requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},                    PERMISSIONS_REQUEST_CODE_ACCESS_READ_CONTACTS);            //等待回调 onRequestPermissionsResult(int, String[], int[]) method        }else{            //没有获得授权,做相应的处理!            getData();        }    }    private void initViews() {        listView= (ListView) findViewById(R.id.listview);        if(Build.VERSION.SDK_INT>9){            listView.setOverScrollMode(View.OVER_SCROLL_NEVER);        }        tv_index= (TextView) findViewById(R.id.index);        sideBar= (SideBar) findViewById(R.id.sideBar);        sideBar.setToastTextView((TextView) findViewById(R.id.tv_toast));        sideBar.setOnTouchTextChangeListener(this);    }    private void getData() {        new Thread(){            @Override            public void run() {                try{                    ContentResolver mResolver=getContentResolver();                    //查询联系人数据,query的参数Phone.SORT_KEY_PRIMARY表示将结果集按Phone.SORT_KEY_PRIMARY排序                    Cursor cursor=mResolver.query(Phone.CONTENT_URI                            ,PHONES_PROJECTION,null,null,Phone.SORT_KEY_PRIMARY);                    if(cursor!=null){                        while (cursor.moveToNext()){                            ContactsModel model=new ContactsModel();                            model.setPhone(cursor.getString(PHONES_NUMBER_INDEX));                            if(TextUtils.isEmpty(model.getPhone())){                                continue;                            }                            model.setName(cursor.getString(PHONES_DISPLAY_NAME_INDEX));                            model.setPhonebook_label(cursor.getString(cursor.getColumnIndex(PHONE_BOOK_LABLE)));                            contactsModelList.add(model);                        }                        cursor.close();                    }                    runOnUiThread(new Runnable() {                        @Override                        public void run() {                            mAdapter=new ContactsListAdapter(ContactsListActivity.this                                    ,contactsModelList);                            mAdapter.setUpdateIndexUIListener(ContactsListActivity.this);                            listView.setAdapter(mAdapter);                            listView.setOnScrollListener(mAdapter);                        }                    });                }catch (Exception e){                    e.printStackTrace();                }            }        }.start();    }    @Override    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {        if (requestCode == PERMISSIONS_REQUEST_CODE_ACCESS_READ_CONTACTS                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {            // 获得授权后处理方法            getData();        }    }    /** *更新tv_index的位置实现移动效果 * */    @Override    public void onUpdatePosition(int position) {        ViewGroup.MarginLayoutParams mp= (ViewGroup.MarginLayoutParams) tv_index.getLayoutParams();        mp.topMargin=position;        tv_index.setLayoutParams(mp);    }    /** *更新tv_index显示label * */    @Override    public void onUpdateText(String mText) {        tv_index.setText(mText);    }    @Override    public void onTouchTextChanged(String s) {        int position=getPositionForSection(s);        listView.setSelection(position);    }    /** *根据传入的section来找到第一个出现的位置 * */    private int getPositionForSection(String s){        for(int i=0;i<contactsModelList.size();i++){            if(s.equals(contactsModelList.get(i).getPhonebook_label())){                return i;            }else if(s.equals("↑")||s.equals("☆")){                return 0;            }        }        return -1;    }}

总结:
  实现联系人列表,主要就是以上介绍的几个步骤,只有熟悉联系人在Android中的存储方式,才能够按照自己的实际情况来决定取哪些字段为自己所用。获取到数据后展示的方式有很多种,github上也有很多现成优秀的控件,我们可以自己使用。但是,能知道其实现原理还是很必要的。
  源码:Demo源码

更多相关文章

  1. Android 音频 OpenSL ES PCM数据播放
  2. Android在内存中读取数据
  3. android 列表 数据显示总结
  4. Android 删除手机联系人,添加手机联系人,更新手机联系人信
  5. Android 数据库Sqlite的使用(1)
  6. android Service之四:传递复杂数据类型的远程服务
  7. 传智播客—Android(二)数据存储和访问 之文件
  8. android 1.6 联系人
  9. Android保存数据几种常用方法解析

随机推荐

  1. AndroidStudio3.0及以上 使用第三方注解
  2. Android开发常用开源框架2
  3. Android(安卓)Studio升级到3.1出现的变化
  4. Android之位置的服务
  5. Android(安卓)监听软键盘状态
  6. 在代码中设置RelativeLayout布局中标签的
  7. 解决:Error: Could not find gradle wrapp
  8. Android开发入门经典资料下载
  9. Android(安卓)xml文件 SAX
  10. 软键盘挤压UI界面问题