获取系统通讯录,自定义通讯录展示:基于RecyclerView实现列表展示。
技术:
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);
}
这里是为了添加一个判断,合并首字母相同的项,将这些项只显示为一类
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 添加长按气泡
使用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 通讯录 个人界面:
这里也是定义一个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
这里的资源文件和样式文件没有把代码贴出来,可以根据自己需求进行更改.
这里贴一下目录结构:
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