跳到主要内容
Kotlin AI 大前端
基于 AR 眼镜的 Android 亲戚称呼助手开发 综述由AI生成 一款基于 Android 和 AR 眼镜的亲戚称呼助手应用。通过集成特定 SDK,利用蓝牙通信将手机存储的亲戚信息(姓名、称呼、关系、话术)实时同步至 AR 眼镜端显示。文章详细阐述了项目搭建、数据模型设计、蓝牙连接流程、UI 实现及常见坑点(如权限申请、场景控制顺序、编码问题)。该方案解决了春节走亲访友时记不住亲戚称呼的痛点,提供了隐蔽且便捷的交互体验。
涅槃凤凰 发布于 2026/4/6 更新于 2026/5/23 31 浏览基于 AR 眼镜的 Android 亲戚称呼助手开发
需求背景
春节期间走亲访友时,面对一年见一次的亲戚,准确称呼往往成为难题。尤其是刚结婚的新人或不常回家的打工人,容易叫错人造成尴尬。
传统解决方案如手机备忘录、小本本或口头询问存在操作繁琐、不礼貌或不靠谱等问题。利用 AR 眼镜可以实现隐蔽、即时的信息提示,提升社交体验。
技术选型:AR 眼镜方案
相比手机和纸质记录,AR 眼镜具有天然优势:
对比维度 手机 AR 眼镜 使用隐蔽性 众人可见你在查手机 只有自己能看到屏幕内容 操作便捷度 掏出→解锁→搜索→查看 抬眼即见,无需动手 社交压力 明显在看手机,不礼貌 自然地瞟一眼,谁也发现不了 响应速度 打开 APP 需要几秒 信息即时显示
项目搭建:从零开始集成 SDK
1. 创建项目并配置 Maven 仓库
创建一个标准的 Android 项目,使用 Kotlin 语言,minSdk 设为 28(SDK 硬性要求)。在 settings.gradle.kts 中添加对应的 Maven 仓库。
dependencyResolutionManagement {
repositories {
maven { url = uri("https://maven.rokid.com/repository/maven-public/" ) }
google()
mavenCentral()
}
}
2. 添加依赖项
在 app/build.gradle.kts 中引入 SDK 和必要的 Android 组件。
android {
namespace = "com.relativetitle.helper"
compileSdk = 34
defaultConfig {
minSdk = 28
targetSdk = 34
}
}
dependencies {
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2" )
implementation("androidx.core:core-ktx:1.12.0" )
implementation("com.google.android.material:material:1.11.0" )
implementation("androidx.recyclerview:recyclerview:1.3.2" )
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0" )
}
3. 权限声明
眼镜与手机通过蓝牙通信,需要声明相关权限。Android 12+ 对蓝牙权限做了拆分,需要特别注意。
<uses-permission android:name ="android.permission.BLUETOOTH" />
<uses-permission android:name ="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name ="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags ="neverForLocation" />
<uses-permission android:name ="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name ="android.permission.ACCESS_FINE_LOCATION" />
关键提示:Android 12 以上,BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 必须在运行时动态申请,仅 Manifest 声明会导致崩溃。
数据模型:如何存储亲戚信息? 先定义一个简洁的数据结构。每个亲戚条目需要:名字、称呼、关系描述、拜年话术,以及可选的拼音和备注。
data class Relative (
var id: Long = 0 ,
val name: String,
val title: String,
val relation: String,
val greetingTemplate: String,
val phonetic: String? = null ,
val notes: String? = null
) {
fun toGlassDisplayText () : String = buildString {
appendLine("👤 $name " )
appendLine()
appendLine("称呼:$title " )
appendLine("关系:$relation " )
appendLine()
appendLine("────── 拜年话术 ──────" )
appendLine(greetingTemplate)
}
}
这个 toGlassDisplayText() 方法很关键——它决定了信息在眼镜上的呈现方式。我选择了简洁清晰的格式,这样在眼镜上一眼就能看到关键信息。
数据持久化 亲戚数据需要持久化存储。考虑到数据量不大(通常几十条),可以选择 SharedPreferences + JSON 序列化方案。
object RelativeRepository {
private const val PREFS_NAME = "relative_helper_prefs"
private const val KEY_RELATIVES = "relatives"
private val relatives = mutableListOf<Relative>()
fun init (context: Context ) {
loadFromPrefs(context)
if (relatives.isEmpty()) {
loadPresetData()
}
}
fun searchRelatives (keyword: String ) : List<Relative> {
if (keyword.isBlank()) return relatives.toList()
val lowerKeyword = keyword.lowercase()
return relatives.filter {
it.name.contains(keyword, ignoreCase = true ) ||
it.title.contains(keyword, ignoreCase = true ) ||
it.relation.contains(keyword, ignoreCase = true )
}
}
private fun loadPresetData () {
relatives.addAll(listOf(
Relative(1 , "王芳" , "大姨" , "妈妈的姐姐" , "大姨新年好!祝您身体健康,万事如意!" ),
Relative(2 , "李明" , "叔叔" , "爸爸的弟弟" , "叔叔过年好!祝您事业顺利,财源广进!" )
))
}
}
此外,实现了一个贴心的小功能:根据称呼自动生成拜年话术。
fun generateDefaultGreeting (title: String ) : String = when (title) {
"爷爷" , "外公" -> "${title} 新年好!祝您身体健康,长命百岁!"
"奶奶" , "外婆" -> "${title} 新年好!祝您福如东海,寿比南山!"
"姑姑" , "婶婶" , "舅妈" , "大姨" , "小姨" -> "${title} 新年好!祝您青春永驻,越来越年轻!"
"表哥" , "堂哥" -> "${title} 新年好!祝今年发大财!"
else -> "${title} 新年好!祝您新年快乐,万事如意!"
}
核心:眼镜通信模块 这是整个项目最核心的部分——如何让手机和眼镜"对话"?
SDK 封装思路 SDK 提供了 API 类作为通信入口,包含蓝牙连接、场景控制、数据发送等功能。为了方便使用,将其封装成单例对象。
object GlassesManager {
private val api by lazy { Api.getInstance() }
private var connectionCallback: ConnectionCallback? = null
interface ConnectionCallback {
fun onConnecting ()
fun onConnected ()
fun onDisconnected ()
fun onFailed (errorMsg: String )
}
val isBluetoothConnected: Boolean get () = api.isBluetoothConnected
}
蓝牙连接流程 连接眼镜分为两步:先调用初始化方法获取连接信息,再调用连接方法建立实际连接。
fun initBluetoothConnection (context: Context , device: BluetoothDevice ) {
connectionCallback?.onConnecting()
api.initBluetooth(
context = context,
device = device,
callback = object : BluetoothStatusCallback {
override fun onConnectionInfo (socketUuid: String ?, macAddress: String ?, ...) {
if (!socketUuid.isNullOrEmpty() && !macAddress.isNullOrEmpty()) {
connectBluetooth(context, socketUuid, macAddress)
} else {
connectionCallback?.onFailed("获取连接信息失败" )
}
}
override fun onConnected () { connectionCallback?.onConnected() }
override fun onDisconnected () { connectionCallback?.onDisconnected() }
override fun onFailed (errorCode: ValueUtil .CxrBluetoothErrorCode ?) {
connectionCallback?.onFailed(getBluetoothErrorMessage(errorCode))
}
}
)
}
踩坑提醒:SDK 的蓝牙连接是异步的,所有结果都通过回调返回。不要试图同步等待连接结果,会导致死锁。
发送数据到眼镜 数据发送是整个应用的关键功能。这里有一个必须注意的顺序:先打开提词器场景,再发送文本数据。
fun sendTextToGlasses (text: String , callback: SendCallback ? = null ) : Boolean {
if (!isBluetoothConnected) {
callback?.onFailed("眼镜未连接" )
return false
}
openWordTipsScene()
val status = api.sendStream(
type = ValueUtil.CxrStreamType.WORD_TIPS,
stream = text.toByteArray(Charsets.UTF_8),
fileName = "relative_info.txt" ,
cb = object : SendStatusCallback {
override fun onSendSucceed () { callback?.onSuccess() }
override fun onSendFailed (errorCode: ValueUtil .CxrSendErrorCode ?) {
callback?.onFailed(getSendErrorMessage(errorCode))
}
}
)
return status == ValueUtil.CxrStatus.REQUEST_SUCCEED
}
private fun openWordTipsScene () : Boolean {
val status = api.controlScene(
sceneType = ValueUtil.CxrSceneType.WORD_TIPS,
openOrClose = true ,
otherParams = null
)
return status == ValueUtil.CxrStatus.REQUEST_SUCCEED
}
我第一次调试时,发送返回成功,但眼镜端什么都没显示。排查了半天才发现是场景没打开。
TTS 语音播报 除了文字显示,SDK 还支持 TTS(文字转语音)功能,可以在同步信息后播放语音提示。
fun sendTtsFeedback (text: String ) : Boolean {
if (!isBluetoothConnected) return false
val status = api.sendTtsContent(text)
if (status == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
api.notifyTtsAudioFinished()
}
return status == ValueUtil.CxrStatus.REQUEST_SUCCEED
}
注意:调用 sendTtsContent 后,必须再调用 notifyTtsAudioFinished(),否则 TTS 可能播放不完整。
UI 界面:简洁实用优先 主界面采用经典的列表式布局:顶部为眼镜连接状态指示器 + 连接按钮;中间为搜索框 + 亲戚卡片列表;右下角为浮动添加按钮。
布局结构
<androidx.coordinatorlayout.widget.CoordinatorLayout >
<com.google.android.material.appbar.AppBarLayout >
<androidx.appcompat.widget.Toolbar app:title ="亲戚称呼助手" />
</com.google.android.material.appbar.AppBarLayout >
<LinearLayout >
<androidx.cardview.widget.CardView >
<LinearLayout >
<View android:id ="@+id/connection_indicator" />
<TextView android:id ="@+id/tv_connection_status" />
<MaterialButton android:id ="@+id/btn_connect" />
</LinearLayout >
</androidx.cardview.widget.CardView >
<TextInputLayout app:startIconDrawable ="@drawable/ic_search" >
<TextInputEditText android:id ="@+id/et_search" />
</TextInputLayout >
<RecyclerView android:id ="@+id/recycler_view" />
</LinearLayout >
<FloatingActionButton android:id ="@+id/fab_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout >
列表项卡片 每张卡片展示一个亲戚的关键信息,并提供"同步到眼镜"按钮。
<androidx.cardview.widget.CardView >
<LinearLayout android:orientation ="vertical" >
<TextView android:id ="@+id/tv_name" android:textSize ="18sp" android:textStyle ="bold" />
<LinearLayout android:orientation ="horizontal" >
<TextView android:id ="@+id/tv_title" android:background ="@drawable/bg_tag" />
<TextView android:id ="@+id/tv_relation" />
</LinearLayout >
<TextView android:id ="@+id/tv_greeting" android:maxLines ="2" android:ellipsize ="end" />
<Button android:id ="@+id/btn_sync" android:text ="同步到眼镜" />
</LinearLayout >
</androidx.cardview.widget.CardView >
Activity 核心逻辑
class MainActivity : AppCompatActivity () {
private val glassesManager = GlassesManager
override fun onCreate (savedInstanceState: Bundle ?) {
RelativeRepository.init (this )
glassesManager.setConnectionCallback(object : ConnectionCallback {
override fun onConnected () {
updateConnectionStatus(true )
Snackbar.make(binding.root, "眼镜连接成功" , Snackbar.LENGTH_SHORT).show()
}
override fun onDisconnected () {
updateConnectionStatus(false )
}
})
binding.etSearch.addTextChangedListener { text ->
filterRelatives(text?.toString() ?: "" )
}
}
private fun syncToGlasses (relative: Relative ) {
if (!glassesManager.isBluetoothConnected) {
Snackbar.make(binding.root, "请先连接眼镜" , Snackbar.LENGTH_SHORT).show()
return
}
val text = relative.toGlassDisplayText()
glassesManager.sendTextToGlasses(text, object : SendCallback {
override fun onSuccess () {
Snackbar.make(binding.root, "已同步到眼镜" , Snackbar.LENGTH_SHORT).show()
glassesManager.sendTtsFeedback("${relative.title} 的信息" )
}
override fun onFailed (errorMsg: String ) {
Snackbar.make(binding.root, "同步失败:$errorMsg " , Snackbar.LENGTH_LONG).show()
}
})
}
}
踩坑实录:那些文档没告诉你的事
坑一:蓝牙权限动态申请 Android 12(API 31)对蓝牙权限做了重大调整,将原来的 BLUETOOTH 和 BLUETOOTH_ADMIN 拆分为更细粒度的 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT。
关键点:这些新权限必须运行时动态申请。仅 Manifest 声明在 Release 版本中会直接崩溃。
private fun checkAndRequestPermissions () : Boolean {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION
)
} else {
arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_FINE_LOCATION
)
}
val notGranted = permissions.filter {
ActivityCompat.checkSelfPermission(this , it) != PackageManager.PERMISSION_GRANTED
}
if (notGranted.isEmpty()) return true
ActivityCompat.requestPermissions(this , notGranted.toTypedArray(), REQUEST_PERMISSIONS)
return false
}
坑二:提词器场景必须先打开 这个问题让我调试了整整一下午。发送调用返回成功,但眼镜端什么都没显示。
最后发现:必须先调用控制场景方法打开提词器场景,才能发送数据。
api.controlScene(WORD_TIPS, true , null )
api.sendStream(WORD_TIPS, data , ...)
坑三:中文编码 第一次发送中文内容,眼镜上显示一堆乱码。原因是 toByteArray() 默认使用系统编码,在某些设备上可能不是 UTF-8。
stream = text.toByteArray()
stream = text.toByteArray(Charsets.UTF_8)
最终效果
功能清单 功能 状态 说明 亲戚列表 ✅ 支持按名字/称呼/关系搜索 添加/编辑/删除 ✅ 表单输入,自动生成话术 眼镜连接 ✅ 自动发现已配对设备 眼镜同步 ✅ 一键发送称呼 + 话术 TTS 播报 ✅ 语音反馈同步成功 预设数据 ✅ 内置常见亲戚数据
眼镜端显示效果 当你在手机上点击"同步到眼镜"后,眼镜屏幕上会立即显示关键信息。
┌─────────────────────────────┐
│ 👤 王芳 │
│ │
│ 称呼:大姨 │
│ 关系:妈妈的姐姐 │
│ │
│ ────── 拜年话术 ────── │
│ 大姨新年好! │
│ 祝您身体健康,万事如意! │
└─────────────────────────────┘
春节拜年时,当亲戚走过来,你只需要悄悄瞟一眼眼镜,称呼和话术尽收眼底,从容应对,再也不会叫错人了。
总结与展望 这个项目从想法到完成只用了两天时间,代码量不大(约 800 行),但确实解决了一个真实痛点。
SDK 的正确使用方式(场景控制 + 数据发送的顺序问题)
Android 12+ 蓝牙权限的正确处理
简洁实用的数据模型设计
加入语音识别,说"这个是谁"自动识别并显示
支持拍照识别亲戚(需要人脸识别技术)
关系图谱可视化,直观展示家族关系
云端数据同步,换手机不丢数据
相关免费在线工具 RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
随机西班牙地址生成器 随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online