Android 通讯录展示

获取系统通讯录,自定义通讯录展示:基于RecyclerView实现列表展示。

image

技术:

RecyclerView、首字母排序(汉字转拼音)、侧边栏View实现、PopupWindow(气泡)。

1、创建通讯录实体类:

public class Contact implements Serializable {
  private String letter;  // 首字母
  private String name; // 姓名
  private String number; // 号码

...
}

这里继承了Serializable接口,方便Activity之间传递对象数据。

2、获取通讯录:

2.1:申请权限:

  private void initData() {
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
                != PackageManager.PERMISSION_GRANTED){
            String[] list = { Manifest.permission.READ_CONTACTS };
            ActivityCompat.requestPermissions(this, list, 1);
        }else {
            readContacts();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 1){
            if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                readContacts();
            }
        }
    }

2.2 获取通讯录信息:

并将通讯录数据简单处理存入ArrayList中

2.2.1

获取通讯录数据

ContentResolver contentResolver = this.getContentResolver();
        Cursor cursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                null, null, null, null);
        cursor.getCount();
        ArrayList<Contact> data = new ArrayList<>();
        while(cursor.moveToNext()) {
            String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
            String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
            Contact contact = new Contact();
            contact.setName(name);
            contact.setNumber(number);
            data.add(contact);
        }
        cursor.close();

        mContact = filledData(data);
        Collections.sort(mContact, mComparator);

Collections.sort(mContact, mComparator);是用于对数据进行排序的。
mComparator是自定义的一个工具类。

2.2.2

对通讯录数据进行排序处理:

// 为list填充数据
    private ArrayList<Contact> filledData(ArrayList<Contact> data){
        ArrayList<Contact> list = new ArrayList<>();
        for (int i = data.size() - 1; i >= 0; i--) {
            Contact sm = new Contact();
            sm.setName(data.get(i).getName());
            sm.setNumber(data.get(i).getNumber());
            String pinyin = mParser.getSelling(data.get(i).getName());
            String sortString = pinyin.substring(0, 1).toUpperCase();
            if (sortString.matches("[A-Z]")) {
                sm.setLetter(sortString);
            } else {

                sm.setLetter("#");
            }
            list.add(sm);
        }
        return list;
    }

这里mParser用到了一个工具类:

private CharacterParser mParser = CharacterParser.getInstance();

用于将汉字转为拼音。

2.2.3 定义布局文件:

main_activity.xml,
view_contact.xml,
布局可根据自身需求定义。

2.2.4 实现Adapter:

常规RecycleView的Adapter定义,这里还添加了一个点击监听和长按监听。以及对通讯录数据进行了简单处理:同一人含有多个号码时;进行合并:

public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.MyViewHolder> {
    private ArrayList<Contact> mContact;
    public MyItemOnClickListener mListener;
    public MyItemOnLongClickListener mLongListener;

    public static class MyViewHolder extends RecyclerView.ViewHolder {
        private TextView nameView;
        private TextView numberView;
        private TextView letterView;
        private TextView tv_item_tag;
        private RelativeLayout userView;

        public MyViewHolder(View view) {
            super(view);
            nameView = (TextView) view.findViewById(R.id.name);
            numberView = (TextView) view.findViewById(R.id.number);
            letterView = (TextView) view.findViewById(R.id.letter);
            tv_item_tag = (TextView) view.findViewById(R.id.tv_item_tag);
            userView = (RelativeLayout) view.findViewById(R.id.user);
        }
    }

  // 通讯录数据处理
    public ContactAdapter(ArrayList<Contact> mContact) {
        for (int i = 0; i < mContact.size(); i++) {
            for (int j = i + 1; j < mContact.size(); j++) {
                if (mContact.get(i).getName().equals(mContact.get(j).getName())) {
                    String number = mContact.get(i).getNumber() + "\n" + mContact.get(j).getNumber();
                    mContact.get(i).setNumber(number);
                    mContact.remove(j);
                }
            }
        }

        this.mContact = mContact;
    }

    @NonNull
    @Override
    public ContactAdapter.MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.view_contact, parent, false);

        return new MyViewHolder(v);
    }

    @Override
    public void onBindViewHolder(@NonNull final MyViewHolder holder, final int position) {
        String name = String.valueOf(mContact.get(position).getName());
        String number = String.valueOf(mContact.get(position).getNumber());
        String letter = String.valueOf(mContact.get(position).getLetter());
        if (!letterCompareSection(position)) {
            holder.tv_item_tag.setText(letter);
            holder.tv_item_tag.setVisibility(View.VISIBLE);
        } else {
            holder.tv_item_tag.setVisibility(View.GONE);
        }
        holder.nameView.setText(name);
        holder.numberView.setText(number);
        holder.letterView.setText(letter);
        if(mListener != null){
            holder.userView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    try {
                        mListener.onItemOnClick(v, mContact.get(position));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        if(mLongListener != null){
            holder.userView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    try {
                        mLongListener.onItemLongClick(v, mContact.get(position));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    return true;
                }
            });
        }
    }

    @Override
    public int getItemCount() {
        return mContact.size();
    }

    private Boolean letterCompareSection(int position) {
        if (position == 0) {
            return false;
        }
        String letter1 = mContact.get(position).getLetter();
        String letter2 = mContact.get(position - 1 ).getLetter();
        Boolean result = letter1.equals(letter2);
        return result;
    }

    public void setOnItemClickListener(MyItemOnClickListener listener){
        this.mListener = listener;
    }

    public void setOnItemLongClickListener(MyItemOnLongClickListener listener) {
        this.mLongListener = listener;
    }

    public interface MyItemOnClickListener {
        void onItemOnClick(View view, Contact contact) throws IOException;
    }

    public interface MyItemOnLongClickListener {
        void onItemLongClick(View view, Contact contact) throws IOException;
    }

}
注意:
if (!letterCompareSection(position)) {
            holder.tv_item_tag.setText(letter);
            holder.tv_item_tag.setVisibility(View.VISIBLE);
        } else {
            holder.tv_item_tag.setVisibility(View.GONE);
        }

这里是为了添加一个判断,合并首字母相同的项,将这些项只显示为一类


image.png

Android 中 RecyclerView加载过程中,不是一次吧所有的View全部加载出来的,而是只加载界面能展示的项加上预加载的项,所有有未加载的项加载时,他会从内存中寻找是否有已加载的view,来实现服用,减少资源占用。所以要添加:

holder.tv_item_tag.setVisibility(View.VISIBLE);

让其保证该展示的都展示,不然会存在不展示的现象。详情可了解RecyclerView加载机制。

2.2.6

MainActivity.java 初始化RecycleView布局:

binding.recyclerview.setHasFixedSize(true);
        layoutManager = new LinearLayoutManager(this);
        binding.recyclerview.setLayoutManager(layoutManager);
        viewAdapter = new ContactAdapter(mContact);
        viewAdapter.setOnItemClickListener(new ContactAdapter.MyItemOnClickListener() {
            @Override
            public void onItemOnClick(View view, Contact contact) throws IOException {
                Intent intent = new Intent(MainActivity.this, UserActivity.class);
                intent.putExtra("contact", contact);
                startActivity(intent);
            }
        });
        viewAdapter.setOnItemLongClickListener(new ContactAdapter.MyItemOnLongClickListener() {
            @Override
            public void onItemLongClick(View view, final Contact contact) throws IOException {
                initPopWindow(view, contact);
            }
        });
        binding.recyclerview.setAdapter(viewAdapter);
2.3 添加长按气泡
image.png

使用PopupWindow控件
这里可以根据菜鸟教程上的介绍学习使用
我这里进行了简单修改,添加了在上部展示.
MainActivity.java:

viewAdapter.setOnItemLongClickListener(new ContactAdapter.MyItemOnLongClickListener() {
            @Override
            public void onItemLongClick(View view, final Contact contact) throws IOException {
                initPopWindow(view, contact);
            }
        });
private void initPopWindow(View v, final Contact contact) {
        View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.bubble_dialog, null, false);
        Button btn_xixi = (Button) view.findViewById(R.id.buttonDelete);
        //1.构造一个PopupWindow,参数依次是加载的View,宽高
        final PopWindowView popWindow = new PopWindowView(MainActivity.this, view);

//                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);

        popWindow.setAnimationStyle(R.anim.anim_pop);  //设置加载动画

        //这些为了点击非PopupWindow区域,PopupWindow会消失的,如果没有下面的
        //代码的话,你会发现,当你把PopupWindow显示出来了,无论你按多少次后退键
        //PopupWindow并不会关闭,而且退不出程序,加上下述代码可以解决这个问题
        popWindow.setTouchable(true);
        popWindow.setTouchInterceptor(new View.OnTouchListener() {
//            @Override
//            public boolean onTouch(View v, MotionEvent event) {
//                return false;
//            }

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return false;
                // 这里如果返回true的话,touch事件将被拦截
                // 拦截后 PopupWindow的onTouchEvent不被调用,这样点击外部区域无法dismiss
            }
        });
        popWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));
        popWindow.getBackground().setAlpha(0);    //要为popWindow设置一个背景才有效

        //设置popupWindow显示的位置,参数依次是参照View,x轴的偏移量,y轴的偏移量
        popWindow.showUp2(v, 300, 50);

        //设置popupWindow里的按钮的事件
        btn_xixi.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mContact.remove(contact);
                viewAdapter.notifyDataSetChanged();
                popWindow.dismiss();
            }
        });
    }

重写类:PopWindowView,添加在上部显示气泡,
如果没有这个需求可以不用写此项.直接使用PopupWindow.

public class PopWindowView extends PopupWindow {
    private int popupWidth;
    private int popupHeight;
    public PopWindowView(Context context, View view) {
        super(context);
        setPopConfig(view);
    }

    /**
     *
     * 配置弹出框属性
     * @version 1.0
     *
     * @createTime 2015/12/1,12:45
     * @updateTime 2015/12/1,12:45
     * @createAuthor
     * @updateAuthor
     * @updateInfo (此处输入修改内容,若无修改可不写.)
     *
     */

    private void setPopConfig(View view ) {
        this.setContentView(view);//设置要显示的视图
        this.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
        this.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
        this.setOutsideTouchable(true);// 设置外部触摸会关闭窗口

        //获取自身的长宽高
        view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
        popupHeight = view.getMeasuredHeight();
        popupWidth = view.getMeasuredWidth();
    }

    /**
     * 设置显示在v上方(以v的左边距为开始位置)
     * @param v
     */
    public void showUp(View v) {
        //获取需要在其上方显示的控件的位置信息
        int[] location = new int[2];
        v.getLocationOnScreen(location);
        //在控件上方显示
        showAtLocation(v, Gravity.NO_GRAVITY, (location[0]) - popupWidth / 2, location[1] - popupHeight);
    }

    /**
     * 设置显示在v上方(以v的中心位置为开始位置)
     * @param v
     */
    public void showUp2(View v) {
        //获取需要在其上方显示的控件的位置信息
        int[] location = new int[2];
        v.getLocationOnScreen(location);
        //在控件上方显示
        showAtLocation(v, Gravity.NO_GRAVITY, (location[0] + v.getWidth() / 2) - popupWidth / 2, location[1] - popupHeight);
    }

    /**
     * 设置显示在v上方(以v的中心位置为开始位置)
     * @param v, x, y
     */
    public void showUp2(View v, int x, int y) {
        //获取需要在其上方显示的控件的位置信息
        int[] location = new int[2];
        v.getLocationOnScreen(location);
        //在控件上方显示
        showAtLocation(v, Gravity.NO_GRAVITY, (location[0] + v.getWidth()/2) - popupWidth / 2 + x, location[1] - popupHeight + y);
    }

}
2.4 侧边栏View:

MainActivity.java应用:

 binding.viewSidebar.setLetterTouchListener(new SideBarView.LetterTouchListener(){
            @Override
            public void setLetter(String letter) {
                for(int i = 0 ; i < mContact.size(); i++ ){
                    if(letter.equals(mContact.get(i).getLetter())){
                        binding.recyclerview.scrollToPosition(i);
                    }
                }
            }
        });

SideBarView: 自定义View,主要实现侧边栏.

2.5 通讯录 个人界面:
image.png

这里也是定义一个RecyclerView,将MainActivity传过来的值进行展示,这里不做详细讲解了.

Contact user = (Contact)intent.getSerializableExtra("contact");
        if(user != null){
            String number = user.getNumber();
            numbers = number.split("\\n");
            letter = user.getLetter();
            name = user.getName();
        }

就是对号码进行简单处理,将字符串转成数组.
getSerializableExtra()方法是intent传对象时使用的,实体类要继承Serializable接口.
这里还有一个动态申请打电话的权限,

 numberAdapter.setOnItemClickListener(new UserNumberAdapter.MyItemOnClickListener() {
            @Override
            public void onItemOnClick(View view, String number) throws IOException {
                selectNumber = number;
                if(ContextCompat.checkSelfPermission(UserActivity.this, Manifest.permission.CALL_PHONE)
                        != PackageManager.PERMISSION_GRANTED){
                    String[] list = { Manifest.permission.CALL_PHONE };
                    ActivityCompat.requestPermissions(UserActivity.this, list, 1);
                }else {
                    call();
                }
            }
        });

重写回调

 @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 1){
            if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                call();
            }else{
                Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
            }
        }
    }

call(),调用系统接口,实现打电话.

private void call(){
        try {
            if(selectNumber != null){
                Intent intent = new Intent(Intent.ACTION_CALL);
                intent.setData(Uri.parse("tel: " + selectNumber));
                startActivity(intent);
            }
        }catch (SecurityException e){
            e.printStackTrace();
        }
    }
2.6

这里的资源文件和样式文件没有把代码贴出来,可以根据自己需求进行更改.
这里贴一下目录结构:


image.png
image.png

9.png图片是绘制的9-Patch图片,也可根据自己需求进行更改.

2.7

注意:
不要忘了添加权限申请

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

引用:
七月雨:[Android 按字母排序的通讯录] https://blog.csdn.net/QQ55214/article/details/81204402;
LuZhenBangBlog:[PopupWindow显示在某个控件上方] https://blog.csdn.net/lu1024188315/article/details/51786656

3 项目地址:

https://github.com/ios-lcj/Android-Address-book

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。