当前位置: 首页 > news >正文

安卓开发---通信录的UI实例

效果图

系统的架构设计

Java类文件

①MainActivity.java-通讯录主页面

②ContactSyn.java-联系人数据模型

③SectionItem.java-分组项数据模型(区分标题和联系人)

④ContactSynAdapter.java-RecyclerView适配器

⑤StickyHeaderDecoration.java-粘性头部效果实现

XML布局文件

①activity_main.xml-通讯录界面布局

②contact_syn_item_.xml-联系人列表项布局

③item_header.xml-字母分组标题布局

数据流动的路径

系统通讯录 → ContactSyn对象 → 分组排序 → SectionItem列表 → RecyclerView显示

联系人处理逻辑

原始联系人 → 拼音转换 → 按拼音排序 → 按首字母分组 → 构建分段列表 → UI显示
↓           ↓           ↓           ↓            ↓         ↓
姓名/电话   ZHANGSAN    排序后列表    A,B,C分组   [头部A,联系人1,联系人2,头部B...]  分组显示

关键功能

1.粘性头部的实现

2.侧边导航栏的实现

代码实例

先在AndroidManifest.xml中添加相关权限

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

在build.gradle.kts中添加相关依赖并同步

implementation(libs.jpinyin)

MainActivity.java

package com.example.myapplication;import android.Manifest;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;import com.github.stuxuhai.jpinyin.PinyinException;
import com.google.android.material.bottomsheet.BottomSheetDialog;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;public class MainActivity extends AppCompatActivity {private static final String TAG = "MainActivity";//请求码private static final int REQUEST_READ_CONTACTS = 100;//UIprivate RecyclerView recyclerView;//联系人列表private ContactSynAdapter adapter;//列表适配器private LinearLayout sideIndex;//右侧字母导航栏private ImageView allSelected;private boolean isAllSelected = false;private TextView allCancel;//数据private List<SectionItem> sectionItems = new ArrayList<>();//分组数据(头部+联系人)private List<String> initials = new ArrayList<>();//字母索引列表@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//初始化initView();initClick();initRecyclerView();initPermission();}//获取视图private void initView() {recyclerView = findViewById(R.id.recycler_contacts);sideIndex = findViewById(R.id.side_index);allSelected = findViewById(R.id.iv_select);allCancel = findViewById(R.id.tv_cancel);}//点击事件private void initClick() {// 全选按钮点击事件allSelected.setOnClickListener(v -> {isAllSelected = !isAllSelected; // 切换状态selectAllContacts(isAllSelected);// 更新全选按钮图标allSelected.setImageResource(isAllSelected ?R.mipmap.select_icon : R.mipmap.unselect_icon);});//全部取消按钮点击事件allCancel.setOnClickListener(v -> {// 点击取消按钮 -> 全部取消isAllSelected = false;selectAllContacts(false);// 同时把全选按钮的图标切换成未选中allSelected.setImageResource(R.mipmap.unselect_icon);});}//权限检查private void initPermission() {//检查是否有通讯录权限if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {//没有权限加载权限ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);} else {//有权限直接加载通讯录loadContacts();}}//设置适配器private void initRecyclerView() {recyclerView.setLayoutManager(new LinearLayoutManager(this));//创造线性布局adapter = new ContactSynAdapter(sectionItems);//创建适配器recyclerView.setAdapter(adapter);//绑定适配器// 添加粘性头部装饰器:实现分组悬停效果recyclerView.addItemDecoration(new StickyHeaderDecoration(new StickyHeaderDecoration.HeaderProvider() {//判断某个位置是否是头部项@Overridepublic boolean isHeader(int position) {//告诉装饰器哪些位置是头部return sectionItems.get(position).getType() == SectionItem.TYPE_HEADER;}//创建并返回头部视图@Overridepublic View getHeader(RecyclerView parent, int position) {//获取视图View header = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_header, parent, false);//获取头部文本视图TextView tvHeader = header.findViewById(R.id.tv_header);//设置头部文本内容tvHeader.setText(sectionItems.get(position).getHeader());//返回配置好的头部视图return header;}}));}//权限回调@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {if (requestCode == REQUEST_READ_CONTACTS) {if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {//权限授予就加载通讯录loadContacts();} else {Toast.makeText(this, "需要通讯录权限", Toast.LENGTH_SHORT).show();}}super.onRequestPermissionsResult(requestCode, permissions, grantResults);}//获取通讯录数据private void loadContacts() {List<ContactSyn> contacts = new ArrayList<>();//查询系统联系人数据?????????Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,  //联系人的URInull, null, null, null);//查询所有字段if (cursor != null) {while (cursor.moveToNext()) {//获取姓名和电话String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));String phone = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));try {//创建联系人对象并添加到列表contacts.add(new ContactSyn(name, phone));} catch (PinyinException e) {throw new RuntimeException(e);}}cursor.close();}// 对联系人进行排序和分组sortAndGroupContacts(contacts);// 设置右侧字母导航栏setupSideIndex();adapter.notifyDataSetChanged();}//排序和分组方法private void sortAndGroupContacts(List<ContactSyn> contacts) {sectionItems.clear();initials.clear();// 按拼音排序Collections.sort(contacts, (c1, c2) -> c1.getPinyin().compareTo(c2.getPinyin()));String lastInitial = "";for (ContactSyn contact : contacts) {//获取首字母String initial = contact.getInitial();if (!initial.equals(lastInitial)) {//添加分组头部sectionItems.add(new SectionItem(initial));initials.add(initial);lastInitial = initial;}//添加联系人项sectionItems.add(new SectionItem(contact));}}//设置侧边导航private void setupSideIndex() {//清空现有视图sideIndex.removeAllViews();for (String initial : initials) {TextView textView = new TextView(this);textView.setText(initial);textView.setTextSize(12);textView.setGravity(Gravity.CENTER);textView.setPadding(0, 8, 0, 8);//点击字母跳转到对应分组textView.setOnClickListener(v -> scrollToSection(initial));sideIndex.addView(textView);}}//跳转到指定分组private void scrollToSection(String initial) {for (int i = 0; i < sectionItems.size(); i++) {SectionItem item = sectionItems.get(i);//找到匹配的头部项if (item.getType() == SectionItem.TYPE_HEADER && item.getHeader().equals(initial)) {//滚动到该位置((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPositionWithOffset(i, 0);break;}}}// 批量操作的方法需要更新,使用selected状态public void selectAllContacts(boolean select) {for (SectionItem item : sectionItems) {if (item.getType() == SectionItem.TYPE_CONTACT) {item.getContact().setSelected(select);}}adapter.notifyDataSetChanged();}}

SectionItem.java

package com.example.myapplication;public class SectionItem {public static final int TYPE_HEADER = 0;//分组头部public static final int TYPE_CONTACT = 1;//联系人项private int type;// 类型:TYPE_HEADER 或 TYPE_CONTACTprivate String header;// 如果是头部,存储字母(如"A", "B")private ContactSyn contact;// 如果是联系人,存储联系人对象//构造方法 - 创建头部public SectionItem(String header) {this.type = TYPE_HEADER;this.header = header;}//构造方法 - 创建联系人public SectionItem(ContactSyn contact) {this.type = TYPE_CONTACT;this.contact = contact;}public int getType() { return type; }public String getHeader() { return header; }public ContactSyn getContact() { return contact; }
}

StickyHeaderDecoration.java

package com.example.myapplication;import android.graphics.Canvas;
import android.view.View;
import android.view.ViewGroup;import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {private final HeaderProvider headerProvider; //头部提供者接口private View currentHeader;//当前显示的粘性头部视图//定义头部提供者接口public interface HeaderProvider {boolean isHeader(int position);//判断某个位置是否是头部View getHeader(RecyclerView parent, int position);//获取头部视图}//构造函数public StickyHeaderDecoration(HeaderProvider headerProvider) {this.headerProvider = headerProvider;}@Overridepublic void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {super.onDrawOver(c, parent, state);//1.获取屏幕上最顶部的子视图View topChild = parent.getChildAt(0);if (topChild == null) return;//2.获取顶部子视图在适配器中的位置int topChildPos = parent.getChildAdapterPosition(topChild);if (topChildPos == RecyclerView.NO_POSITION) return;// 3.找到当前需要固定的Header位置int headerPos = findHeaderPosition(topChildPos);if (headerPos == -1) return;//4.获取头部视图并确保布局currentHeader = headerProvider.getHeader(parent, headerPos);ensureHeaderLayout(parent, currentHeader);// 绘制Header到Canvasint saveCount = c.save();c.translate(0, Math.max(0, topChild.getTop() - currentHeader.getHeight()));currentHeader.draw(c);c.restore();}//查找头部位置:从当前位置向上查找最近的头部private int findHeaderPosition(int position) {for (int i = position; i >= 0; i--) {if (headerProvider.isHeader(i)) {return i;//返回找到的头部位置}}return -1;//没有找到头部}//确保头部布局private void ensureHeaderLayout(RecyclerView parent, View header) {//设置布局参数if (header.getLayoutParams() == null) {header.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,//宽度匹配父容器ViewGroup.LayoutParams.WRAP_CONTENT));//高度包裹内容}//测量头部视图int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);//布局头部视图header.measure(widthSpec, heightSpec);header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());}//完整工作流程// 1. 用户滚动列表
// 2. RecyclerView触发onDrawOver()
// 3. 找到当前屏幕顶部的位置
// 4. 向上查找最近的头部位置
// 5. 获取头部视图并测量布局
// 6. 计算绘制位置(考虑头部被推出的情况)
// 7. 在Canvas上绘制头部
}

ContactSynAdapter.java

package com.example.myapplication;import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;import java.util.List;public class ContactSynAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {private List<SectionItem> sectionItemsInAdapter;//数据源:包含头部和联系人的混合列表//构造函数public ContactSynAdapter(List<SectionItem> items) {this.sectionItemsInAdapter = items;}//视图类型的判断@Overridepublic int getItemViewType(int position) {return sectionItemsInAdapter.get(position).getType();//返回当前位置的类型}@NonNull@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {if (viewType == SectionItem.TYPE_HEADER) {//创建头部视图View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_header, parent, false);return new HeaderViewHolder(v);} else {//创建联系人项视图View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.contact_syn_item, parent, false);return new ContactViewHolder(v);}}@Overridepublic void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {SectionItem item = sectionItemsInAdapter.get(position);//获取当前位置的数据项if (holder instanceof HeaderViewHolder) {//如果是头部ViewHolder,绑定头部数据((HeaderViewHolder) holder).bind(item.getHeader());} else if (holder instanceof ContactViewHolder) {//如果是联系人ViewHolder,绑定联系人数据((ContactViewHolder) holder).bind(item.getContact());}}//返回总项数@Overridepublic int getItemCount() {return sectionItemsInAdapter.size();}//头部视图持有者static class HeaderViewHolder extends RecyclerView.ViewHolder {TextView tvHeader;//头部文本视图HeaderViewHolder(View itemView) {super(itemView);tvHeader = itemView.findViewById(R.id.tv_header);//绑定视图}void bind(String header) {tvHeader.setText(header);}//设置头部文本}//联系人视图持有者static class ContactViewHolder extends RecyclerView.ViewHolder {TextView tvName, tvPhone;//姓名和电话ImageView ivSelect;//选中状态图标ContactViewHolder(View itemView) {super(itemView);//获取视图tvName = itemView.findViewById(R.id.tv_name);tvPhone = itemView.findViewById(R.id.tv_phone);ivSelect = itemView.findViewById(R.id.iv_select);}//联系人数据绑定void bind(ContactSyn contact) {//1.设置显示数据tvName.setText(contact.getName() != null ? contact.getName() : "");tvPhone.setText(contact.getPhone() != null ? contact.getPhone() : "");// 2.设置选中状态图标ivSelect.setImageResource(contact.isSelected() ? R.mipmap.select_icon : R.mipmap.unselect_icon);// 3.选择图标的点击事件ivSelect.setOnClickListener(v -> {boolean newState = !contact.isSelected();//切换选中状态contact.setSelected(newState);//更新数据,标记当前项是选中,还是非选中ivSelect.setImageResource(newState ? R.mipmap.select_icon : R.mipmap.unselect_icon);//更新UI图标});// 4.整个item的点击事件:提供更大的点击区域itemView.setOnClickListener(v -> {boolean newState = !contact.isSelected();contact.setSelected(newState);ivSelect.setImageResource(newState ? R.mipmap.select_icon : R.mipmap.unselect_icon);});}}}

ContactSyn.java

package com.example.myapplication;import com.github.stuxuhai.jpinyin.PinyinException;
import com.github.stuxuhai.jpinyin.PinyinFormat;
import com.github.stuxuhai.jpinyin.PinyinHelper;/*通讯录同步中的联系人的类*/
public class ContactSyn {private String name,phone;//名字和电话private boolean selected;//是否选中private String pinyin; // 拼音全拼private String initial; // 首字母//构造函数public ContactSyn(String name, String phone) throws PinyinException {this.name = name;this.phone = phone;this.selected = false;//默认未选中// 转换为拼音(不带音调)try {//name要转换的中文名字//”“分隔符//WITHOUT_TONE不包含音调符号// .toUpperCase(),变为大写,this.pinyin = PinyinHelper.convertToPinyinString(name, "", PinyinFormat.WITHOUT_TONE).toUpperCase();} catch (PinyinException e) {throw new RuntimeException(e);}// 获取首字母//.substring(0,1)从第一位开始截取,且只截取一位this.initial = PinyinHelper.getShortPinyin(name).substring(0, 1).toUpperCase();}//只读属性public String getName() { return name; }public String getPhone() { return phone; }public String getPinyin() { return pinyin; }public String getInitial() { return initial; }//可读写属性public boolean isSelected() { return selected; }public void setSelected(boolean selected) { this.selected = selected; }}

item_header.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="48dp"android:background="#F5F5F5"android:padding="8dp"><TextViewandroid:id="@+id/tv_header"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="16sp"android:textColor="#333"/></LinearLayout>

contacy_syn_item.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:app="http://schemas.android.com/apk/res-auto"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="70dp"><ImageViewandroid:id="@+id/iv_select"android:layout_width="17dp"android:layout_height="17dp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintTop_toTopOf="parent"tools:ignore="MissingConstraints"android:layout_marginStart="20dp"/><TextViewandroid:id="@+id/tv_name"android:layout_width="0dp"android:layout_weight="1"android:layout_height="wrap_content"android:text="联系人姓名"android:textStyle="bold"android:textSize="18dp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"android:layout_marginStart="65dp"android:layout_marginTop="11.5dp"tools:ignore="MissingConstraints"/><TextViewandroid:id="@+id/tv_phone"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="电话"android:textSize="15sp"tools:ignore="MissingConstraints"app:layout_constraintStart_toStartOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:layout_marginStart="65dp"android:layout_marginBottom="13dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"><!-- 联系人列表 --><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_contacts"android:layout_width="0dp"android:layout_height="wrap_content"app:layout_constraintTop_toTopOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintBottom_toTopOf="@+id/cl_bottom"android:layout_marginTop="30dp"/><!-- 侧边索引栏 --><LinearLayoutandroid:id="@+id/side_index"android:layout_width="24dp"android:layout_height="wrap_content"android:orientation="vertical"android:gravity="center_horizontal"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="@id/recycler_contacts"android:layout_marginBottom="20dp"app:layout_constraintBottom_toBottomOf="@id/recycler_contacts"/><!-- 底部操作栏 --><androidx.constraintlayout.widget.ConstraintLayoutandroid:id="@+id/cl_bottom"android:layout_width="match_parent"android:layout_height="80dp"android:background="@color/white"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"><!-- 选择图标 --><ImageViewandroid:id="@+id/iv_select"android:layout_width="17dp"android:layout_height="17dp"android:src="@mipmap/unselect_icon"app:layout_constraintStart_toStartOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:layout_marginStart="40dp"android:layout_marginBottom="42.5dp"/><!-- 全选文本 --><TextViewandroid:id="@+id/tv_select_all"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="全选"android:textSize="15dp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:layout_marginStart="67.5dp"android:layout_marginBottom="44dp"tools:ignore="MissingConstraints" /><!-- 取消按钮 --><TextViewandroid:id="@+id/tv_cancel"android:layout_width="101dp"android:layout_height="37dp"android:text="取消全选"android:gravity="center"android:background="#D5D6D8"android:textColor="#999999"app:layout_constraintStart_toStartOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:layout_marginStart="130dp"android:layout_marginBottom="32dp"tools:ignore="MissingConstraints" /><!-- 批量同步按钮 --><TextViewandroid:id="@+id/tv_sync"android:layout_width="101dp"android:layout_height="37dp"android:text="批量同步"android:gravity="center"android:background="#268BF9"android:textColor="@color/white"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:layout_marginEnd="21dp"android:layout_marginBottom="32dp"tools:ignore="MissingConstraints" /></androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

http://www.dtcms.com/a/427600.html

相关文章:

  • 35互联做的网站效果图制作教程
  • Gitee - IDEA 主支 master 和分支 dev 的使用
  • grep 命令处理文件差集
  • MySQL终极备份指南:用Percona XtraBackup实现零数据丢失!
  • FPGA实现SRIO图像视频传输,基于Serial Rapidlo Gen2,提供6套工程源码和技术支持
  • 网站推广渠道有哪些加盟编程教育哪家好
  • GitOps实战:Helm一键部署ArgoCD
  • 聊城冠县网站建设无锡seo公司哪家好
  • 一个专业做设计的网站软件工程师前景及待遇
  • 为 CPU 减负:数据中心网络卸载技术的演进
  • phpstudy配置网站北京网站建设公司哪家最好
  • 《考研408数据结构》第三章(3.1 栈)复习笔记
  • 徐州网站排名工地模板图片大全
  • ARM Cortex-X 与 Cortex-A 命名正式退役,推出C1 CPU和G1 GPU
  • 南昌汉邦网站建设网页设计论文题目大全
  • 上市公司环境信息披露质量评分数据-王婉菁版(2008-2023)
  • 网站底部悬浮一个网站怎么绑定很多个域名
  • 极简全营养三食材组合:土豆 + 鸡蛋 + 绿叶菜
  • Java【代码 24】AOI数据获取(通过地址名称获取UID在获取AOI数据)
  • 提升 HarmonyOS 开发效率:DevEco Studio 6.0 热更新调试模式全指南
  • 桌面预测类开发,桌面%性别,姓名预测%系统开发,基于python,scikit-learn机器学习算法(sklearn)实现,分类算法,CSV无数据库
  • 用自己服务器做网站2023营业执照年检
  • QCustomPlot 高级扩展与实战案例
  • C语言形式参数和实际参数的区别(附带示例)
  • 医疗领域的数智化转型与智能化变革研究报告:技术驱动、模式创新与政策协同
  • 在 C# 中,如何使 $““ 字符串支持换行
  • 2025年精选单北斗GNSS水库形变监测系统对比推荐
  • Java 在Word 文档中添加批注:高效文档协作的利器
  • 代做效果图网站项目管理软件的作用
  • 广东省高水平建设专业网站北京大兴网站建设