news 2026/9/8 17:52:00

Kotlin + 无障碍服务打造老人极简桌面:一键视频通话实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kotlin + 无障碍服务打造老人极简桌面:一键视频通话实战

去年国庆回老家,我发现爷爷的手机桌面乱得可怕。壁纸是他孙女的照片,但上面叠了一层又一层购物推送、直播入口和“清理加速”的小红点。他不敢乱点,又总误触,每次想把屏幕调回拨号界面都要找半天。更扎心的是,他儿子在外地,好不容易休息想视频聊天,爷爷却接不起来——要么锁屏后不知道怎么划,要么在视频应用里找不到右下角的“拨打”按钮。那种无助感让我当场决定:我要动手写一个真正的“老人桌面”,把桌面关进笼子里,只留下一键视频通话这条路。

这个项目是一个基于 Kotlin 的 Android 极简桌面,核心能力就两件事:第一,把第三方桌面替掉,桌面只放几个超大按钮;第二,用无障碍服务做系统层能力兜底,让老人按下任何一个联系人卡片时,能稳定发起语音或视频通话。适合家里有不会用智能手机长辈的开发者参考;也适合想了解 无障碍服务(AccessibilityService)在适老场景里怎么合规落地的朋友,这篇文章会把我们从原理到实现的完整过程,包括所有踩过的坑,全部讲透。

1. 为什么做这个项目:一次和数字鸿沟的正面交手

1.1 老人用智能机最痛的三件事

先说观察。我不会做需求调研,只用了三天时间坐在爷爷旁边看他操作手机,发现了三个极其规律的问题。

第一是找不到入口。桌面放了几页下载的图标,老人靠颜色和位置记图标。一旦系统自动更新导致图标移位,或者有应用弹了个全屏广告,他就会彻底迷路,只能关机重启。

第二是点不准。他的手指有轻微抖动,目标控件只要小于 80dp,几乎不可能点中。系统设置字体调到最大后,很多界面会变成两行省略号,反而更难辨别。第三个问题最致命,是“回不去”。就算他瞎猫碰到死耗子打开了视频通话软件,也不知道怎么回到“能看清联系人头像”的地方;而在通话界面,他不小心按到挂断旁边的小麦克风图标,还会因为画面变化以为自己把对方弄丢了。

所以这个项目的第一个需求非常朴素:桌面必须有且只有一个“入口”的感觉。不需要通知栏、不需要多任务、不需要壁纸滚轮、不需要应用抽屉。它只需要把一个联系人列表做成超大卡片——姓名、头像、大按钮,一个卡片就是一整条可点击区域。其余的,全部交给系统默认能力或无障碍服务去补。

1.2 市面上的极简桌面为什么救不了场

动手之前,我花了一个晚上装了市面上七八款宣称“老人桌面”的应用。结果让我非常失望。

有一款桌面确实能做成大图标,但首次配置需要打开十几个开关;另外一款主界面莫名其妙地放了“今日天气”“幸运抽奖”等模块,老人还没点联系人,就先被弹出的“红包待领取”吸引走。更让我不能接受的是,部分应用要读取位置、通讯录、存储权限,目的却和老人桌面无关。

有人说,Android 有自带的简易模式。我也试过。极简模式确实靠谱,但它基本只放大了系统自带应用;家人需要视频通话时,原始聊天软件还是那个复杂的界面。我想要的是能够“以人为中心”的桌面,而不是“以 App 为中心”的应用列表,市面通用产品很难做到。我必须自己做。

如果单从实现角度讲,做一个桌面并不难;难的是桌面之外的体验闭环。比如默认桌面权限冲突、系统回收桌面、无障碍服务没能监听到老人误触等,这些才是决定项目成败的地方。

1.3 想清楚产品边界:只做一件事

做这类产品最忌讳大而全。我给自己定的边界很简单:极简桌面只面向一个用户——我爷爷。第一版只要支持三位联系人:儿子、女儿、孙子。每位联系人对应一个大卡片,点击后用系统层能力发起语音或视频通话;长按卡片可以编辑联系人。

极简到只剩一个核心流程,其实是有意为之。很多“适老应用”失败,不是功能太少,而是给了老人太多选择。一个人的工作记忆有限,75 岁以后面对超过三个以上的操作可能性时,决策成本会急剧提升。产品边界收缩,反而让每个功能可以被反复练习,形成肌肉记忆。

这也意味着很多高级能力应该做成“自动化的后台服务”,而不是暴露给用户。系统什么时候自动开免提、什么时候提示语音播报、桌面服务是否还被系统保持在前台,这些都需要用无障碍服务的监听能力来接管。

2. 技术选型:Kotlin、无障碍服务与自定义桌面的分工

2.1 自定义 Launcher 承担“看得见”的部分

Android 允许第三方应用把自己声明成桌面(Launcher),只要在 AndroidManifest 里声明包含android.intent.category.HOMEandroid.intent.category.DEFAULT的 Activity,系统设置里就会出现“默认桌面”选项,用户选择本应用后,按 Home 键就会进入它。

这个机制非常成熟,我自己只用了不到半天时间就把桌面主体跑通。每个联系人卡片都是用 Compose 写的,因为 Kotlin + Jetpack Compose 做列表、大按钮、主题定制,比传统 View 系统省掉大量冗余代码。我想让卡片足够大、文字足够重、背景和前景的对比度足够高,Compose 的声明式风格在调 UI 时几乎不用思考双重绑定问题。

桌面应用本质上就是一个常驻系统前台的普通应用,它不需要后台服务也能显示。所以“看得见”部分,实际承担的是人机交互主界面:卡片、字体、电话拨打按钮、来电状态提示、防止误触取消上一次可能误触的操作。

2.2 无障碍服务承担“看不见”的部分

真正的难点是“视频通话”。想要做到一键发视频,我们需要处理很多系统层面不一致的问题:不同品牌的设备对默认拨号应用的呈现逻辑不同;多数聊天软件不会提供公开 Intent 给第三方直接发起视频呼叫;老人在通话过程中卡在某个授权弹窗的情况也时有发生。

如果每个环节都要安卓原生应用自己去承担,那就是灾难。于是这个项目第二块核心技术登场:无障碍服务(AccessibilityService)。它能替应用读取当前界面节点,也能模拟手势操作,还能感知窗口状态变化和服务是否被系统回收。

在合规边界内,我让无障碍服务承担了四个“看不见”的任务:第一,检测老人是否把桌面切到了后台,如果超时没有切回来,自动用系统语音提醒;第二,在联络人卡片被点击后,通过窗口状态监测确认通话界面是否真的在前台,而不是让老人误切到了别的界面;第三,在部分系统需要额外权限弹窗时,辅助自动点击“允许”,减少老人阅读弹窗的负担;第四,感知用户一段时间内没有触摸操作,自动把屏幕调到高亮并播放引导语音。

2.3 为什么没选模拟点击和自动化脚本方案

在早期调研时,我其实考虑过一个更野的路子:直接给桌面做一个后台脚本,每隔几百毫秒截屏找按钮,再模拟点击目标联系人。但很快我放弃了。

粗暴模拟点击最大的问题是脆弱。手机屏幕的像素密度、主题字体、系统应用版本一变,截图识别的位置可能全部失效。服务端没有返回值,你根本无法通过系统 API 确认当前是不是在通话,只能自己做图像推断。无障碍服务虽然不是银弹,但至少它是 Android 系统公开、稳定的辅助能力,事件是系统回调给我们的,而不是靠图像识别去猜。维护成本、稳定性和功耗,都比模拟点击方案好一个数量级。

另外一个原因更重要:合规。模拟点击、后台截屏、全局悬浮窗点按,这类方案在法律和用户隐私层面都很危险。无障碍服务同样需要告知用户开启,但在使用目的和商家政策上更透明。我没有做任何“绕过”或“隐藏”的尝试,所有权限入口都明确标注:本服务仅用于辅助完成桌面联系人通话和误触恢复,不读取任何无关信息。

2.4 Kotlin 在这个项目中的实际价值

选择 Kotlin 并不算追新。我的开发环境是 Android Studio 最新稳定版,Kotlin 2.x,Compose UI,整个工程几乎没有 Java 代码。Kotlin 的优势在这个项目里主要体现在三处。

一是协程方便处理异步事件。无障碍服务回调大量线程上的事件,我需要把运行逻辑切到主线程再更新 UI,用lifecycleScopewithContext(Dispatchers.Main)可以减少大量样板代码。二是数据类非常适合表示联系人、配置项;一条联系人记录就是data class Contact(val id: Long, val name: String, val avatarUri: String?, val phone: String)。三是Flow的无障碍事件收集体验很好,特别是桌面服务处理系统服务绑定状态变化时,可以把它变成响应式流,配合项目里简单的状态管理,比 BroadcastReceiver 清爽得多。

Kotlin 不会直接解决“老人误触”问题,但它能让开发者把精力集中在处理用户痛点上,而不是和回调地狱纠缠。

3. 先把无障碍服务的地基打好

3.1 无障碍服务的运行流程

无障碍服务本质上是一个系统级绑定服务。用户需要去系统“无障碍”设置里找到你的应用,手动开启开关。之后,它会在整个系统后台运行,不是你桌面应用退到后台就会断掉,而是有一个独立的系统绑定关系。

这个绑定关系很关键。系统会把当前界面中发生的无障碍事件,源源不断地发给你的服务。事件类型包括:窗口状态变化、点击、长按、文本改变、滚动、应用切到前台。我们关心的主要是TYPE_WINDOW_STATE_CHANGEDTYPE_WINDOW_CONTENT_CHANGED;前者用于感知“当前是否已经切到了通话界面”,后者用于发现“界面上出现了弹窗需要处理”。

一旦绑定成功后,系统会回调onAccessibilityEvent(AccessibilityEvent?)onInterrupt()。你在这里处理的其实是系统层的输入/输出事件流,不是在跑一个扫描屏幕的爬虫。理解这一点,有助于后续正确设计代码。

3.2 关键 XML 配置拆解

无障碍服务需要两个配置文件。一个是标准的服务注册,在res/xml/accessibility_service_config.xml,另一个是 AndroidManifest 中将服务与配置关联。

核心 XML 长这样:

<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeNotificationStateChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canPerformGestures="true" android:canRetrieveWindowContent="true" android:description="@string/accessibility_service_description" android:notificationTimeout="100" />

这段配置里有两个字段我建议重点理解。

canRetrieveWindowContent="true"表示服务可以读取当前窗口的内容节点树。想要判断界面里是否存在某个文本、是否处于通话状态,必须开这个能力。canPerformGestures="true"表示可以执行手势,比如向下滑动、点击某个坐标。我通常不用点击坐标,只依赖节点信息执行Action,但万一遇到没有语义标签的图像按钮,可能需要用手势来完成“返回”操作。

还需要注意notificationTimeout。这个值设得太小,事件会像洪水一样涌进服务,很容易变成耗电大户;设得太大,一些涉及安全确认的弹窗可能来不及响应。实测设成 100 毫秒比较平衡。

3.3 常用的几个无障碍 API

项目里最常用的 API 是这几个,直接给你列出场景:

  • AccessibilityNodeInfo代表当前窗口里的一个控件节点。我可以拿到一个文本按钮、一个联系人卡片、甚至一个输入框。代码示例:
fun findButtonByText(nodeInfo: AccessibilityNodeInfo?, text: String): AccessibilityNodeInfo? { if (nodeInfo == null) return null if (nodeInfo.text?.toString()?.contains(text) == true) return nodeInfo for (i in 0 until nodeInfo.childCount) { val child = nodeInfo.getChild(i) ?: continue val result = findButtonByText(child, text) if (result != null) return result } return null }
  • performAction(AccessibilityNodeInfo.ACTION_CLICK)对某个控件模拟点击。这个操作等价于用户手指点了一下目标,系统会做完整的事件分发,应用无感知,主要用来确权或回到桌面。

  • dispatchGesture无障碍服务能直接画一条手势路径。为了保险,我更多用GestureDescription执行“从屏幕底部向上滑返回桌面”,在日常使用中比performGlobalAction(GLOBAL_ACTION_HOME)更贴近系统手势。

  • isServiceEnabled判断 通过Settings.Secure读取ENABLED_ACCESSIBILITY_SERVICES,看当前应用的无障碍服务是否在启用列表里。这用于首页显示“当前辅助服务已开启/未开启”的提示,不会越权。

3.4 生命周期与服务异常处理

最容易出问题的是系统回收和重启。普通应用退到后台后,进程可能被杀掉,无障碍服务会随之被系统自动重启;但重启后你应该恢复状态:桌面是否在当前前台、是否已经弹过欢迎语音。我的处理是维护了一个AccessibilityStateRepository,在服务onCreate时把一份持久化的状态快照恢复出来,onDestroy时保存。服务意外崩溃后,重启流程能很快回到健康态。

真正需要小心的是onInterrupt()。这个回调什么时候触发?可能是系统资源紧张、服务被系统暂停、也可能是用户去无障碍设置里把你关掉。这个场景下一定不能做重量级操作,我的策略只做一件事:记录一条日志,然后等待下一个 onAccessibilityEvent 时重新初始化。

4. 极简桌面和一键通话是怎么落地的

4.1 桌面真正“极简”的视觉交互

极简不等于白屏。老人对色块和头像的识别远远强于文字,所以桌面视觉上要“大而稳”。我按这个规则设计:

  • 每个联系人卡片高度不小于 180dp,文字不小于 32sp,名字带超大字体绿色标题。
  • 状态栏、通知栏默认全部隐藏,防止老人误触下拉产生混乱。
  • 主屏最多显示 3 张卡片,每张卡片中间放一个直径约 96dp 的圆形联系人头像,头像下方只有名字和一个“视频通话”标签。
  • 不允许横向滚动,没有应用抽屉,没有文件夹。

这里有一个细节:卡片要覆盖整个父容器宽度。老人手指点击时经常没有落在中心,如果卡片左右还有留白,很容易点不中。我把卡片的clickable区域和控件实际高度做了统一,并且把触摸反馈改成明显的浅色遮罩,按下去就有很强的反馈感。设计并不复杂,但每一条都要实际让老人上手验证。我记得自己最开始做的是两列排布,后来被爷爷证实“一行一列”更好——他点第二个联系人时,不会误触第一个。

4.2 把应用声明为一个桌面

桌面声明相当成熟,具体做法是在AndroidManifest.xml里给MainActivity加一个 Intent Filter:

<activity android:name=".MainActivity" android:excludeFromRecents="true" android:launchMode="singleTask" android:screenOrientation="portrait" android:theme="@style/Theme.SeniorLauncher"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.HOME" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>

但是,应用安装后系统不会自动把权限给你。你要在应用首次启动时,检测自己是不是默认桌面。可以使用一个很低调的方法:

private fun isDefaultLauncher(): Boolean { val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_HOME) } val resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) val currentPackage = resolveInfo?.activityInfo?.packageName ?: return false return currentPackage == packageName }

如果检测出来不是默认桌面,我给用户展示一个极大按钮:“把我设为默认桌面”,点击后跳转系统桌面选择设置页Settings.ACTION_HOME_SETTINGS。注意,很多老机型的系统桌面设置页长得很不一样;我先发送一个提示 Toast,再打开对应页面,几秒钟内不到 5 秒就会看到系统桌面选择弹窗。真正做好这一步,需要真机慢慢测。

4.3 一键视频通话的实现细节:先把通话做得可靠

桌面做好了,核心功能就是按键后接通。这句话说得容易,实际落地时要狠下心做一次产品取舍。

Android 系统层对“拨打电话”有相对清晰的 Intent:

fun dial(phoneNumber: String, video: Boolean = false) { val uri = Uri.fromParts("tel", phoneNumber, null) val intent = if (video) { Intent(Intent.ACTION_VIDEO_CALL, uri) } else { Intent(Intent.ACTION_DIAL, uri) } if (intent.resolveActivity(packageManager) != null) { startActivity(intent) } else { // 降级用系统默认拨号 startActivity(Intent(Intent.ACTION_DIAL, uri)) } }

语音通话用ACTION_DIAL不需要敏感权限,ACTION_CALL才需要CALL_PHONE,我会优先前者,让系统进入拨号盘后,老人再按一下绿色拨号键。但要注意,这中间多了一步,并不符合“一键”。

实际上,我最终采用的方案是加一层底部的“大绿色拨号键”:点卡片先弹出一个确认面板,面板上只有一个“立即呼叫”按钮。为什么不直接点卡片?因为在实测中,卡片很容易被误触。对防误触的兜底来说,无论如何也必须有一个确认动作。这一步虽然多花 0.5 秒,但能避免老人想点视频却误打了语音电话造成紧张。

至于视频通话,一个残酷的真相是:Android 并没有系统级标准 Intent 能让第三方聊天软件像播电话一样发起视频。不同品牌有自己的通话界面,第三方 IM 应用则需要通过它们各自的协议去拉通。我没有去写针对某家 IM 的私有 hook,而是做了两层设计:

  • 如果系统有支持ACTION_VIDEO_CALL的通话应用,就直接用它发起视频通话;
  • 如果系统不能处理,自动降级为语音呼叫,并通过无障碍服务监听系统前台窗口,一旦发现通话界面没起来,会在大屏上给出明显的语音提示:“请点击最上面的绿色按钮”。

在第一版产品阶段,功能稳定比“视频”这个字眼更重要。我不会为了追求“一定走某家 App 视频”而破坏桌面后用户的操作路径。未来,可以预留一个自定义 Intent 方案,让家人在管理界面手动配置好点击流程,但作为长辈使用者,他不会也不应看到这些配置项。

4.4 联系人数据存储方式

这个桌面不需要网络,不需要账号体系,只需要本地联系人配置。我使用的是一张最简单的 Room 表:

CREATE TABLE contacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT NOT NULL, avatar_uri TEXT, sort_order INTEGER DEFAULT 0 )

开发启动器时,别把联系人缓存放到内存里。系统可能因为桌面切换频繁杀掉 MainActivity,而用户配置好的联系人如果只保存在内存中,会直接丢配置。Room 数据库配合StateFlow,代码相当简洁,大概一百行就能完成访问。

还需要提供一个简单的“管理模式”。默认情况桌面不能退出,防止老人误触;但家属长按右上角的隐藏小锁图标 3 秒,就可以进入管理页面,增删联系人、更换头像、重新指定默认桌面。管理界面的 Auth 提示明确:这个入口仅给家人使用,不会进入老人主桌面。

4.5 用无障碍服务兜底:自动检测桌面现状与误触恢复

桌面真正在系统里跑起来后,有一个比 APP 崩溃更烦人的问题:老人可能被系统通知栏、来电通知、某些弹窗带出桌面,回不来。这个环节,无障碍服务可以完成“最后一道守护”。

我在AccessibilityService里监听窗口状态变化,每次事件都会拿当前前台窗口包名与桌面包名做对比:

override fun onAccessibilityEvent(event: AccessibilityEvent?) { if (event == null) return if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { val pkgName = event.packageName?.toString() val inForeground = pkgName == packageName if (!inForeground && !isCallScreenActive(event)) { // 不在通话界面,也离开了桌面:进入兜底模式 handleWanderAway() } } }

这背后的逻辑是“它不是活跃在第三方界面中,而是系统诱导切换导致我离开了主场景”。一旦判断老人离开桌面超过两分钟,且又不在通话状态,我就自动把桌面带回前台,同时播放本地合成语音:“你已经回到联系人桌面。如果想打电话,请点绿色大按钮。”这个兜底非常管用,我实测了三周,老人基本不再“迷路”。

需要注意一个细节:不要每次离开桌面都立刻切回。比如来电响铃、系统闹钟弹出、用户主动去拨号盘确认时,盲目切回会引起反感。必须分析窗口类型。我处理的策略是:只有在前台 App 确实是老人在不知情情况下点进的应用详情页、系统设置、安装推荐页等高风险包名时,才执行自动回桌面;对其他包名只记录日志。

5. 实机踩坑与适配记录

5.1 服务开了又被系统偷偷杀掉

无障碍服务最大的敌人不是代码,而是各家 ROM 的后台清理策略。很多手机默认开启“省电模式”“不必要的后台限制”,会把我们的服务标记为“可清理”。表现是:桌面 App 还活着,但从设置里看无障碍服务开关已经自动关掉了,或者一直没有任何回调。

这个问题的根因是无障碍服务依赖应用进程常驻;进程被杀,服务自然断开。解决办法有三层:

  • 在 AndroidManifest 里给服务声明android:foregroundServiceType或使用前台服务通知,尽量提升进程优先级;
  • 代码里监听系统ACTION_MY_PACKAGE_REPLACEDBOOT_COMPLETED等事件,在开机后主动引导用户重新开启服务;
  • 在应用首页做一个“服务健康检测”:如果发现无障碍服务没有开启,用传感器类型检测服务状态,显示一个极大的红色提示条。

实际上,没有任何公开 API 能让 Android 保证你的进程在二手机后台永不被清理。我的经验是:别做永久驻留的“永动机”,把状态提示做清楚更重要。至少家人看到应用首页的红色提示条,知道要重新开启服务。系统这个限制反而逼着我意识到,无障碍服务只做“兜底”而不是“依附”。

5.2 各家 ROM 的开关入口差异很大

老人们的手机几乎没有一部是 Pixel,华为、小米、OPPO、vivo 数量最多。不同系统开启无障碍服务的设置路径差别很大,甚至开关文案都可能不同。最开始我给应用写了一个“去开启”按钮,跳转Settings.ACTION_ACCESSIBILITY_SETTINGS,发现不同手机上定位到的列表完全不一样,部分系统还会强制让你去它的“安全中心”开启“自启动权限”。

针对这个情况,我的引导界面用了图形化的步骤截图。但在应用内部不方便内置太多 ROM 截图,后来我干脆做了一个扫码展示网页,网页里按品牌分类给出了详细贴图说明。页面非常简单,但对不熟悉技术的家属帮助极大。

这一条,实测比代码本身值钱。你要记住一个原则:不能用跳转一个通用设置页就完事,必须针对主流品牌给出差异化的引导路径。

5.3 无障碍服务不能滥用

开发过程中我一直有一个底线:我既然要辅助老人,就不能把读到的东西传给任何服务器,更不能用来做第三方的数据挖掘。无障碍服务可以拿到整个窗口的节点树,这意味着用户屏幕上的文本几乎对开发者透明;但恰恰因为权限很大,Android 官方和各家应用商店对无障碍服务审核都非常严格。如果你的应用在无障碍权限下做了与功能无关的自动点击、读取短信、自动下载,商店很容易下架,甚至会进入系统级黑名单。

在实现兜底时,我只在事件回调里临时读取packageName和几个关键content-desc/文本,立即使用、立即丢,不落盘、不截图、不上传。工程代码里其实可以做到按事件过滤,只监听桌面相关的窗口变化即可;其他包名一律早期返回,不做任何分析。合规,是这个项目能长期跑下去的根基。

5.4 误触带来的用户信任问题,防重复点击算法

老人的手指肌肉控制不太好,点了一次按钮,经常会因为反应慢再补一次。如果防重复点击没做好,会出现一个严重问题:他想给儿子打视频,但由于补点,把电话挂了或者取消通话。因此,我代码里加入了一个非常简单的扩展函数:

private var lastClickTime = 0L fun View.safeClick(debounceMs: Long = 1500L, action: () -> Unit) { setOnClickListener { val now = SystemClock.elapsedRealtime() if (now - lastClickTime < debounceMs) { return@setOnClickListener } lastClickTime = now action.invoke() } }

这个不是标准库,是我自己的工具函数。把防抖时间设置为 1500 毫秒非常关键,太长老人会觉得没反应,太短起不到防误触作用。我可以告诉你,第一版我设置了 800 毫秒,被爷爷的“重复点按”直接破防,他连续点了两下,第一下电话刚呼出,第二下就按到了挂断。后来改成 1500 毫秒,问题解决。

5.5 关于字体、屏幕常亮和通话状态的一些细节

还有三条细节测试记录我觉得值得分享。第一,系统“字体大小”只影响部分控件,Compose 里如果用了固定sp字号的 Text,不会自动缩放,所以设计时我用sp但配合LocalDensity.current进行适配,保证最大字体时也能完整显示。第二,桌面要开屏幕常亮吗?不能开。一直常亮会烧屏,也会导致老人误触到系统锁屏。我改成:老人所在房间光线偏暗时,通过环境光传感器把屏幕亮度调高,但超过三十分钟未操作仍按系统休眠策略。第三,通话状态的判断不要只看窗口标题,也要看TelephonyManager.CALL_STATE_OFFHOOK;因为桌面可能和通话应用不在同一个 Task,无障碍事件不一定每次都精准。

这些小细节很不显眼,但单独拎出来都能省下大量线上运维时间。做适老应用,很多时候不是炫技,而是把一个简单入口做到不焦虑,用户才敢信任你。

6. 无障碍服务在适老场景的更多想象

6.1 从一键视频通话到状态守护

做好一键视频通话之后,我其实没有停在这个功能上。许多独居老人真正怕的不是“不会拨号”,而是“在外面没人知道出了事”。手机上现在有大量传感器系统能力,你完全可以把它和无障碍服务结合起来。

例如:通过系统广播检测老人手机是否连续一天没有解锁屏幕;通过加速度传感器判断是否发生剧烈撞击;通过无障碍服务检测老人是否停留在紧急拨号界面超过两分钟。这些信号综合起来,可以向家属发一条本地通知或通过短信接口发送提醒。这里不需要自己处理云端链路,把事件拼成一个标准文本广播就足够。

无障碍服务在这里依然承担“合理读取窗口状态并判定场景”的职责。它比普通应用多了一份观测窗口事件的权限,也因此更应该被克制地使用在每个关键决策节点上,不做无意义的全盘扫描。

6.2 生态协作建议:适老应用不应该靠单个应用单打独斗

我写完这个项目后,和几位做智慧养老的朋友聊过,大家一致认为:真正适合老人的数字环境应该是一套组合拳。极简桌面负责入口,无障碍服务负责系统兜底,通话软件负责联系人关系,设备厂商负责底层稳定。单打独斗很容易产生冲突,比如桌面自己做了视频通话,就会希望第三方 IM 开放更多接口;除非系统原生支持,否则靠一个独立应用去适配所有软件几乎不现实。

所以如果你准备做类似项目,可以考虑走“轻桌面”路线:把精力放在 Home 选择、大字体、防误触、服务状态检测、紧急呼叫这五个核心能力上。把视频通话能力留在原生拨号或系统联系人的通话能力内,不要试图去碰某个聊天软件的私有界面。这既是对用户负责,也是给开发者自己减压。

6.3 项目后续方向

这个项目目前已经在爷爷手机上稳定运行了六个月,我也把它开放成了一个个人维护的小工程。代码库结构并不复杂;后续我会在 GitHub 上慢慢放出精简版本。计划中的下一步是增加定位守护:如果老人在深夜走出常驻小区超过 200 米,通过本地规则发送提醒,仍不采集云端数据。扩展方向还包括让联系人头像显示成动态大图。每当老人心情不好的时候,可以点一下那个照片,让它播放一小段家人提前录好的语音,作为家里的“数字安慰剂”。无障碍服务仍然是那根连接老人与系统之间的牵引绳,只是绳子能拉动的场景,比我们想象的多很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 17:50:28

爱享素材下载器傻瓜级指南:3步免费抓取视频号抖音视频

爱享素材下载器傻瓜级指南&#xff1a;3步免费抓取视频号抖音视频 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader 爱享素材下…

作者头像 李华
网站建设 2026/9/8 17:47:24

ARM官方MCU关键词识别项目ML-KWS-for-MCU源码深度评测

干嵌入式语音交互这一行的人&#xff0c;多多少少都会碰到一个绕不开的名字&#xff1a;ML-KWS-for-MCU。这是 ARM 官方放出来的一个面向微控制器的关键词识别&#xff08;Keyword Spotting&#xff0c;KWS&#xff09;参考实现&#xff0c;整个项目的训练、模型转换、量化、部…

作者头像 李华
网站建设 2026/9/8 17:45:37

LangChain杂记(python版本)

1.连接大模型先在项目根目录下面建".env"文件&#xff0c;用于存放一些环境变量连接大模型首先要配置好自己的api key和厂商的base url&#xff0c;将这些信息放在.env里面&#xff0c;便于统一管理&#xff0c;也便于后期上传代码到远程仓库时忽略这个文件&#xff…

作者头像 李华
网站建设 2026/9/8 17:45:29

【单片机毕业设计】基于 STM32 的 DHT11 环境感知与 ESP‑01S 无线监控系统设计 基于 STM32 的阈值可配置智能加湿补水报警系统设计(011607)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华