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

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

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

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

   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是号码类型(住宅,手机,座机等等)。改表也是可以扩展的。

  以上几个表都是比较重要的,从这几个表中我们基本上可以满足我们做联系人列表的需求了。这里还要说一下,可能细心一点的话,会发觉有些不同表中有同一字段,这是不是违背了数据库设计规范造成数据冗余呢,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前端怎样php后台交互(基础篇)
  2. Android中ContentProvider详解
  3. Android(安卓)音频 OpenSL ES PCM数据播放
  4. android 列表 数据显示总结
  5. ContentProvider使用与query流程分析
  6. Android(安卓)删除手机联系人,添加手机联系人,更新手机联系人信
  7. Android解析json数组对象
  8. Android的Adapter与BaseAdapter的介绍
  9. widget(4、spinner)

随机推荐

  1. Android Activity去除标题栏和状态栏(z)
  2. android中drawable显示到view上的过程
  3. 控件:拖动条 --- SeekBar(改变屏幕亮度)
  4. android 代码设置apn
  5. Android 通过代码执行Linux 命令 echo 命
  6. android的单个图片 上传服务器
  7. 网址收藏
  8. android 漂亮的listview
  9. 第四例:Intent启动Activity的几种方式(一)
  10. android中如何实现循环更新UI