仿Android6.0聯繫人列表

  最近因爲項目需要,研究了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源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章