cw-omnibus 实战解析:用 Spinner 与 CursorLoader 从 ContactsContract 加载联系人数据的 ContactSpinners 示例
示例工程教程【免费下载链接】cw-omnibusSource code to omnibus edition of _The Busy Coders Guide to Android Development_项目地址https://gitcode.com/gh_mirrors/cw/cw-omnibus点击查看免费下载导读本文基于 cw-omnibus 仓库中的 ContactSpinners 示例应用讲解 Android 中如何通过ContactsContractContentProvider 读取设备联系人并用 Spinner 切换三种查询模式仅姓名、姓名号码、姓名邮箱配合LoaderManagerCursorLoader异步加载数据、用 RecyclerView 渲染结果。读完本文你将掌握联系人读取的投影projection设计、CommonDataKinds.Phone/Email子表查询、危险权限运行时申请以及 Loader 与列表视图的完整联动方案。该示例是《The Busy Coders Guide to Android Development》一书中「ContactsContract 与 CallLog 提供者」章节的配套代码。示例应用概览一个由 Spinner 驱动的联系人查询 Demo官方 README 对该应用的定位非常明确演示从ContactsContractContentProvider 加载数据。它不像许多 Demo 那样把数据源写死而是把「查询什么」交给用户选择——界面顶部是一个 Spinner底部是一个 RecyclerView用户切换 Spinner 选项即触发不同维度的联系人查询Spinner 选项arrays.xml数据来源 URI展示内容Contact NamesContactsContract.Contacts.CONTENT_URI联系人显示名称Contact Names NumbersContactsContract.CommonDataKinds.Phone.CONTENT_URI显示名称 电话号码Contact Names Email AddressesContactsContract.CommonDataKinds.Email.CONTENT_URI显示名称 邮箱地址三个选项分别对应主 Activity 中三个预定义的投影projection与两个 loader 常量具体映射见 ContactSpinners.javaprivate static final int LOADER_NAMES0; private static final int LOADER_NAMES_NUMBERS1; private static final String[] PROJECTION_NAMESnew String[]{ ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME, }; private static final String[] PROJECTION_NUMBERSnew String[]{ ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER }; private static final String[] PROJECTION_EMAILSnew String[]{ ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.CommonDataKinds.Email.DATA };从结构上看可以推断示例有意展示了两类典型的联系人查询路径一类直接查询联系人主表Contacts另一类查询CommonDataKinds下的数据表Phone、Email后者在投影中同时取出DISPLAY_NAME——这是 ContactsContract 中常见的数据组织方式联系人的「聚合信息」位于主表而具体的电话、邮箱等细节分散在关联数据表中。工程结构一览整个示例是一个标准的单模块 Android 工程settings.gradle仅包含include :app核心文件如下主 ActivityContactSpinners.java运行时权限基类AbstractPermissionActivity.java界面布局main.xml菜单数据arrays.xml文案资源strings.xml应用清单AndroidManifest.xml主题资源values/styles.xml 与 values-v21/styles.xml构建配置settings.gradle、project.properties、gradle/wrapper/gradle-wrapper.properties前置准备Manifest 声明与运行时权限处理1. 声明权限读取联系人属于危险权限必须在清单中声明。AndroidManifest.xml 中唯一添加的权限即是uses-permission android:nameandroid.permission.READ_CONTACTS /2. 运行时权限基类 AbstractPermissionActivity由于 READ_CONTACTS 在 Android 6.0API 23之后需要在运行时动态申请示例没有把权限逻辑散落在主 Activity 里而是抽取了一个可复用的抽象基类 AbstractPermissionActivity.java定义三个抽象钩子abstract protected String[] getDesiredPermissions(); abstract protected void onPermissionDenied(); abstract protected void onReady();其核心逻辑在onCreate与onRequestPermissionsResult中模式非常清晰见 AbstractPermissionActivity.java如果已拥有全部所需权限直接回调onReady()进入业务界面否则通过ActivityCompat.requestPermissions()发起申请且只申请「尚未授予」的那部分权限netPermissions()会过滤掉已授权项回调返回后再次检查全部通过则onReady()否则回调onPermissionDenied()。此外基类还做了两项防御性处理用STATE_IN_PERMISSION状态位配合onSaveInstanceState避免旋转屏幕等配置变更导致重复弹出权限对话框请求码固定为REQUEST_PERMISSION61125并在回调中校验requestCode防止其他来源的权限回调串扰。主 Activity 只需实现这三个抽象方法即可。ContactSpinners.java 中权限被拒时给出 Toast 提示并直接finish()Override protected String[] getDesiredPermissions() { return (PERMS); } Override protected void onPermissionDenied() { Toast .makeText(this, R.string.msg_no_perm, Toast.LENGTH_LONG) .show(); finish(); }界面搭建Spinner 与 RecyclerView 的布局与装配布局文件main.xml 是一个非常简洁的垂直 LinearLayout顶部一个 Spinner下方填满剩余空间的 RecyclerView。值得注意的两个细节Spinner android:idid/spinner android:layout_widthmatch_parent android:layout_heightwrap_content android:drawSelectorOnToptrue / android.support.v7.widget.RecyclerView android:idandroid:id/list android:layout_widthmatch_parent android:layout_heightmatch_parent /Spinner 开启了drawSelectorOnTop让选中高亮绘制在内容之上RecyclerView 复用了系统框架保留 IDandroid:id/list这在以AdapterView为主的传统代码中很常见本示例则用它来标识列表容器。Spinner 的数据源Spinner 的选项来自资源数组arrays.xml主 Activity 在onReady()中将其装入ArrayAdapter并显式指定下拉展开时使用的视图资源ContactSpinners.javasetContentView(R.layout.main); Spinner spinfindViewById(R.id.spinner); spin.setOnItemSelectedListener(this); ArrayAdapterString aanew ArrayAdapterString(this, android.R.layout.simple_spinner_item, getResources().getStringArray(R.array.options)); aa.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); spin.setAdapter(aa); RecyclerView rvfindViewById(android.R.id.list); rv.setLayoutManager(new LinearLayoutManager(this)); rv.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); adapternew RVCursorAdapter(getLayoutInflater()); rv.setAdapter(adapter);这里同时示范了 Spinner 装配的三个步骤设置选择监听器、构造ArrayAdapter、通过setDropDownViewResource定制下拉列表样式。列表部分则展示了 RecyclerView 的标准配置LinearLayoutManager纵向布局 DividerItemDecoration分隔线 自定义 Adapter。数据查询LoaderManager 与 CursorLoader 的三种模式主 Activity 实现了LoaderManager.LoaderCallbacksCursor数据加载完全交给CursorLoader在后台线程完成避免在主线程查询数据库导致 ANR。按 loaderId 分发查询onCreateLoader 根据 loaderId 决定投影与 URIswitch (loaderId) { case LOADER_NAMES: projectionPROJECTION_NAMES; uriContactsContract.Contacts.CONTENT_URI; break; case LOADER_NAMES_NUMBERS: projectionPROJECTION_NUMBERS; uriContactsContract.CommonDataKinds.Phone.CONTENT_URI; break; default: projectionPROJECTION_EMAILS; uriContactsContract.CommonDataKinds.Email.CONTENT_URI; break; } return new CursorLoader(this, uri, projection, null, null, ContactsContract.Contacts.DISPLAY_NAME);CursorLoader的五个参数依次为Context、查询 URI、投影列、selection、selectionArgs、排序规则。本示例没有设置任何筛选条件selection 与 args 均为 null但固定按DISPLAY_NAME排序保证列表按联系人名称字母序展示。这里还有一个值得借鉴的编码技巧邮箱模式没有单独声明第三个 loader 常量而是直接使用default分支承接在代码里形成「0姓名、1号码、其余邮箱」的约定配合 Spinner 的 position 使用。结果回调与列映射onLoadFinished 按 loader 的 id 选择不同的展示列并交给 Adapter 换数据switch (loader.getId()) { case LOADER_NAMES: columnsCOLUMNS_NAMES; break; case LOADER_NAMES_NUMBERS: columnsCOLUMNS_NUMBERS; break; default: columnsCOLUMNS_EMAILS; break; } adapter.changeCursor(c, columns);对应的COLUMNS_*数组ContactSpinners.java决定了 RecyclerView 每一行展示哪些列——这正是「投影只决定取哪些列展示列另行声明」的解耦设计private static final String[] COLUMNS_NAMESnew String[]{ ContactsContract.Contacts.DISPLAY_NAME }; private static final String[] COLUMNS_NUMBERSnew String[]{ ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER }; private static final String[] COLUMNS_EMAILSnew String[]{ ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.CommonDataKinds.Email.DATA };Loader 生命周期Loader 框架要求实现三个回调示例三者俱全onLoadFinished 换入新 CursoronLoaderReset 时清空当前 CursorOverride public void onLoaderReset(LoaderCursor loader) { adapter.clearCursor(); }结果渲染RVCursorAdapter 与 RowHolder列表渲染没有使用传统的CursorAdapter而是自定义了RVCursorAdapter extends RecyclerView.AdapterRowHolderContactSpinners.java并复用系统布局android.R.layout.simple_list_item_2——这是一个自带两行文本text1/text2的标准双行列表项。Override public RowHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View row inflater.inflate(android.R.layout.simple_list_item_2, parent, false); return new RowHolder(row); } Override public void onBindViewHolder(NonNull RowHolder holder, int position) { cursor.moveToPosition(position); holder.bind(cursor, columns); } Override public int getItemCount() { return cursornull ? 0 : cursor.getCount(); }RowHolder的绑定逻辑体现了「列数自适应」的设计ContactSpinners.java第一列永远写入text1只有当传入的 columns 数组长度为 2 时才把第二列写入text2从而用同一个 Adapter 同时承载「单列模式」与「双列模式」public void bind(Cursor cursor, String[] columns) { int indexcursor.getColumnIndex(columns[0]); text1.setText(cursor.getString(index)); if (columns.length2) { indexcursor.getColumnIndex(columns[1]); text2.setText(cursor.getString(index)); } }Adapter 还负责 Cursor 的内存管理changeCursor()在替换新 Cursor 前会主动close()旧 CursorclearCursor()则在 loader 重置时置空并刷新列表ContactSpinners.java避免 Cursor 泄漏。交互联动Spinner 选中即切换 Loader整个应用最关键的一行联动代码在 onItemSelectedOverride public void onItemSelected(AdapterView? parent, View v, int position, long id) { getSupportLoaderManager().initLoader(position, null, this); }它巧妙地把 Spinner 的position直接当作loaderId使用position 0 对应「仅姓名」loaderposition 1 对应「姓名号码」loaderposition 2 落入默认分支对应「姓名邮箱」loader。initLoader()的语义是「存在则复用、不存在则创建」因此反复切换选项不会重复创建 loader同一 ID 的 loader 会保留已加载的数据并自动触发重连。onNothingSelected回调则被有意留空因为空选择在此场景下没有意义。主题与资源Holo 与 Material 双版本适配应用主题针对不同 API 级别分别定义values/styles.xml低版本使用 Holo 浅色主题style nameTheme.Apptheme parentandroid:style/Theme.Holo.Light.DarkActionBarvalues-v21/styles.xmlAPI 21 及以上使用 Material 浅色主题并注入品牌色style nameTheme.Apptheme parentandroid:style/Theme.Material.Light.DarkActionBar item nameandroid:colorPrimarycolor/primary/item item nameandroid:colorPrimaryDarkcolor/primary_dark/item item nameandroid:colorAccentcolor/accent/item /style对应的色板定义在 colors.xml主色#3f51b5靛蓝、深主色#1a237e、强调色#ffee58黄。用户拒绝权限时的提示文案在 strings.xml 中维护Sorry! This demo cannot run without permission!。构建与运行该示例工程可直接导入 Android Studiosettings.gradle只声明了:app模块。按仓库根目录 README.markdown 的说明所有示例都带有用于导入的 Gradle wrapper 占位文件导入前务必检查 gradle-wrapper.properties本示例指向 Gradle 4.4以防被链接到被篡改的 Gradle 安装仓库出于安全原因不包含gradlew及其对应 JAR因此命令行构建需要本机自行安装 Gradle根 README 注明为 3.3 版本如需命令行构建在项目根目录执行gradle assembleDebug即可project.properties中标注的targetandroid-19用于传统构建流程现代 Android Gradle 插件环境下以build.gradle中的compileSdkVersion为准。运行时请授予READ_CONTACTS权限应用启动时会自动弹出权限对话框授权后即可看到按姓名排序的联系人列表顶部 Spinner 切换到「姓名号码」或「姓名邮箱」模式列表会立即变为双行显示名称 电话 / 名称 邮箱。若拒绝授权应用会弹出提示并自动退出。小结ContactSpinners 是理解 Android 联系人体系与 Loader 框架的一个小而完整的范本值得反复对照的四点ContactsContract 的两层查询模型主表Contacts提供DISPLAY_NAMECommonDataKinds.Phone/Email提供明细数据且支持在投影中一并取出名称权限处理的可复用封装AbstractPermissionActivity把「检查—申请—回调—重试」的样板逻辑收敛到基类业务 Activity 只需三个抽象方法Loader 与 UI 状态解耦loaderId 即查询模式CursorLoader自动后台加载并跨配置变更保留数据onLoadFinished/onLoaderReset精确控制 Cursor 的换入与清理一个 Adapter 渲染多种列通过动态列数组 复用simple_list_item_2让单列与双列两种展示共用同一套 RecyclerView 适配器。仓库同目录下的 Contacts/Inserter 与 Contacts/CallLog 分别演示了通过ACTION_INSERT写入联系人和查询CallLog提供者可与本文的读取示例对照阅读形成对系统联系人/通话记录数据的完整认知。赞分享示例工程教程【免费下载链接】cw-omnibusSource code to omnibus edition of _The Busy Coders Guide to Android Development_项目地址https://gitcode.com/gh_mirrors/cw/cw-omnibus点击查看免费下载相关推荐使用 ACTION_INSERT 向系统通讯录插入联系人ContactsInserter 实战解析cw-omnibus使用 ACTION_INSERT 向系统通讯录插入联系人ContactsInserter 实战解析cw omnibus 本篇文章以 cw omnibus示例工程教程cw-omnibus 实战用 ActionModeMC 示例为多选 ListView 接入 Action Modecw omnibus 实战用 ActionModeMC 示例为多选 ListView 接入 Action Mode 导读 本文以《The Busy Coder示例工程教程cw-omnibus 实战用 LongPress 示例实现 ListView 长按触发的 Action Modecw omnibus 实战用 LongPress 示例实现 ListView 长按触发的 Action Mode 在 Android 开发中Action M示例工程教程上一篇彻底告别循环嵌套用pandas的3大函数优雅处理数据转换下一篇Zellij跨平台一致性确保各OS体验统一的方法创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考