AR 眼镜拯救社恐:我用 Kotlin 写了个拜年提词器
零、一场社死的春节
大年初一早上七点,我站在二姑家楼下,手心出汗,大脑宕机。
"等会见面...说什么来着?福如东海?不对,那是给老人家的...二姑才五十..."
这已经是我连续第三年拜年"翻车"了。作为一个能徒手撸出复杂状态机的程序员,面对亲戚时却连句完整的吉利话都憋不出来。我爸在旁边疯狂使眼色,我妈事后数落我"读了这么多年书,嘴巴还没三岁小孩利索"。
拜年词穷,本质上是社交场景下的高并发检索问题------你脑子里存了一堆祝福语,但压力上来时,数据库直接锁死,返回 404。
回去的路上,我盯着口袋里的 Rokid AR 眼镜,突然灵光一闪:
为什么不用 AR 眼镜做一个"拜年提词器"?
抬眼就能看到,别人完全察觉不到,简直就是社恐救星。
说干就干,我花了一天时间开发了这个「拜年祝福助手」。
一、技术选型:为什么是 AR 眼镜
在动手之前,我对比了几种方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 手机备忘录 | 简单 | 太明显,像在念稿子 |
| 智能手表 | 隐蔽 | 屏幕太小,翻页困难 |
| 蓝牙耳机 | 完全隐蔽 | 只能听不能看,容易漏听 |
| AR 眼镜 | 抬眼即见,完全隐蔽 | 需要开发 APP |
AR 眼镜的优势太明显了:你只是在"看远方",别人根本不知道你在看提词器。而且 Rokid 提供了完善的 SDK,支持文字投射和语音播报,简直是为此场景量身定做的。
最终技术栈:
- Android 原生开发(Kotlin)
- Material Design 3 组件库
- Rokid CXR-M SDK(眼镜通信)
二、架构设计:简约但不简单
整体架构采用经典的分层设计:

为什么单独抽一层 SDK 封装?因为 Rokid 的蓝牙连接流程比较特殊,需要处理各种回调和状态,封装后上层调用就变得非常简单。
三、开发实战:从零开始
3.1 项目初始化
首先配置 Rokid Maven 仓库,在 settings.gradle.kts 中添加:
scss
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
// Rokid Maven 仓库
maven { url = uri("https://s01.oss.sonatype.org/content/repositories/releases/") }
maven { url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") }
}
}

然后在 app/build.gradle.kts 中引入依赖:
scss
dependencies {
// Rokid CXR-M SDK - 核心通信库
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
// Android 基础库
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
// Lifecycle 组件
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
}

蓝牙权限配置 AndroidManifest.xml:
xml
<!-- 基础蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 12+ 蓝牙权限 -->
<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" />
3.2 数据模型:祝福语分类管理
为了覆盖不同拜年场景,我设计了分类系统:

kotlin
// GreetingTemplate.kt
/**
* 祝福语分类 - 覆盖主要拜年对象
*/
enum class GreetingCategory(val displayName: String) {
ELDERS("长辈"), // 爷爷奶奶、叔叔阿姨
PEERS("同辈"), // 表兄弟、堂姐妹
CHILDREN("晚辈"), // 侄子侄女
COLLEAGUES("同事"), // 工作相关
FRIENDS("朋友") // 死党闺蜜
}
/**
* 祝福语模板
*/
data class GreetingTemplate(
val id: Int,
val category: GreetingCategory, // 分类
val scene: String, // 场景:拜年/发红包/微信拜年
val content: String, // 祝福语内容
val isFormal: Boolean // 是否正式风格
)
然后是内置的祝福语数据库,我精心准备了 25 条不同风格的祝福语:
kotlin
object GreetingData {
val templates: List<GreetingTemplate> = listOf(
// 长辈 - 正式庄重
GreetingTemplate(1, ELDERS, "拜年",
"爷爷奶奶新年好!祝您福如东海、寿比南山!", true),
GreetingTemplate(2, ELDERS, "拜年",
"祝您新春快乐,身体健康,万事如意!", true),
GreetingTemplate(3, ELDERS, "拜年",
"叔叔阿姨过年好!祝您工作顺利,阖家幸福!", true),
GreetingTemplate(4, ELDERS, "发红包",
"恭喜发财,红包拿来!祝您财源广进!", false),
// 同辈 - 轻松有趣
GreetingTemplate(5, PEERS, "拜年",
"新年快乐!今年一起暴富!", false),
GreetingTemplate(6, PEERS, "拜年",
"过年好!祝你脱单成功,升职加薪!", false),
GreetingTemplate(7, PEERS, "发红包",
"恭喜发财,大吉大利!红包走一波!", false),
// 晚辈 - 亲切可爱
GreetingTemplate(8, CHILDREN, "拜年",
"新年快乐!祝你学业进步,快高长大!", true),
GreetingTemplate(9, CHILDREN, "发红包",
"新年好!红包拿去买好吃的!", false),
// ... 更多祝福语
)
// 按分类筛选
fun getByCategory(category: GreetingCategory): List<GreetingTemplate> =
templates.filter { it.category == category }
// 随机获取一条
fun getRandom(category: GreetingCategory): GreetingTemplate =
getByCategory(category).random()
}
3.3 SDK 封装:核心通信逻辑
这是整个项目最核心的部分。Rokid 的蓝牙连接流程比较特殊,需要两步握手:
kotlin
// RokidGlassesManager.kt
object RokidGlassesManager {
private val cxrApi: CxrApi by lazy { CxrApi.getInstance() }
private var connectionCallback: ConnectionCallback? = null
// 连接状态
val isConnected: Boolean
get() = cxrApi.isBluetoothConnected
/**
* 连接眼镜 - 两步流程
* 第一步:initBluetooth 获取连接信息
* 第二步:connectBluetooth 建立实际连接
*/
fun connectGlasses(context: Context, device: BluetoothDevice) {
connectionCallback?.onConnecting()
// 第一步:初始化蓝牙,获取 UUID 和 MAC
cxrApi.initBluetooth(context, device, object : BluetoothStatusCallback() {
override fun onConnectionInfo(
socketUuid: String?,
macAddress: String?,
rokidAccount: String?,
glassesType: Int
) {
// 获取到连接信息,进行第二步连接
if (!socketUuid.isNullOrEmpty() && !macAddress.isNullOrEmpty()) {
connectBluetooth(context, socketUuid, macAddress)
} else {
connectionCallback?.onFailed("获取连接信息失败")
}
}
override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
connectionCallback?.onFailed(errorCode?.name ?: "连接失败")
}
})
}
// 第二步:使用获取的信息建立连接
private fun connectBluetooth(context: Context, uuid: String, mac: String) {
cxrApi.connectBluetooth(context, uuid, mac, object : BluetoothStatusCallback() {
override fun onConnected() {
Log.d("Rokid", "蓝牙连接成功")
}
override fun onDisconnected() {
connectionCallback?.onDisconnected()
}
override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
connectionCallback?.onFailed(errorCode?.name ?: "连接失败")
}
})
}
}
连接成功后,就可以发送祝福语到眼镜了:
kotlin
/**
* 发送祝福语到眼镜显示
* 关键点:必须使用 UTF-8 编码,否则中文乱码
*/
fun sendGreeting(text: String, callback: SendCallback? = null): Boolean {
if (!isConnected) {
callback?.onFailed("眼镜未连接")
return false
}
// 1. 打开提词器场景
cxrApi.controlScene(ValueUtil.CxrSceneType.WORD_TIPS, true, null)
// 2. 发送文本流 - 注意 UTF-8 编码!
val status = cxrApi.sendStream(
type = ValueUtil.CxrStreamType.WORD_TIPS,
stream = text.toByteArray(Charsets.UTF_8), // 关键!
fileName = "greeting.txt",
cb = object : SendStatusCallback() {
override fun onSendSucceed() { callback?.onSuccess() }
override fun onSendFailed(errorCode: ValueUtil.CxrSendErrorCode?) {
callback?.onFailed(errorCode?.name ?: "发送失败")
}
}
)
return status == ValueUtil.CxrStatus.REQUEST_SUCCEED
}
/**
* TTS 语音播报(可选功能)
*/
fun sendTts(text: String): Boolean {
if (!isConnected) return false
val status = cxrApi.sendTtsContent(text)
if (status == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
cxrApi.notifyTtsAudioFinished()
return true
}
return false
}
3.4 UI 界面:简洁实用
界面设计遵循 Material Design 3 规范,采用红色喜庆主题:
xml
<!-- activity_main.xml 核心结构 -->
<LinearLayout android:orientation="vertical">
<!-- 顶部工具栏 - 喜庆红色 -->
<Toolbar
android:background="#D32F2F"
app:title="拜年祝福助手"
app:titleTextColor="#FFFFFF" />
<ScrollView>
<LinearLayout android:padding="16dp">
<!-- 分类选择器 -->
<TextView text="选择祝福对象" />
<ChipGroup android:id="@+id/chipGroup" />
<!-- 祝福语展示卡片 -->
<CardView app:cardCornerRadius="12dp">
<LinearLayout android:gravity="center">
<TextView android:id="@+id/tvScene" /> <!-- 场景标签 -->
<TextView android:id="@+id/tvContent" /> <!-- 祝福语内容 -->
<TextView android:id="@+id/tvPage" /> <!-- 页码 -->
</LinearLayout>
</CardView>
<!-- 翻页按钮组 -->
<LinearLayout android:orientation="horizontal">
<Button android:id="@+id/btnPrev" text="上一条" />
<Button android:id="@+id/btnRandom" text="随机" />
<Button android:id="@+id/btnNext" text="下一条" />
</LinearLayout>
<!-- 眼镜控制区域 -->
<CardView>
<LinearLayout>
<TextView text="Rokid 眼镜" />
<TextView android:id="@+id/tvStatus" text="未连接" />
<Button android:id="@+id/btnConnect" text="连接眼镜" />
<Button android:id="@+id/btnSend" text="发送到眼镜" />
<Button android:id="@+id/btnTts" text="语音播报" />
</LinearLayout>
</CardView>
</LinearLayout>
</ScrollView>
</LinearLayout>
Activity 逻辑实现:
kotlin
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var currentCategory = GreetingCategory.ELDERS
private var currentIndex = 0
private var currentTemplates: List<GreetingTemplate> = emptyList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupChips() // 初始化分类选择器
setupButtons() // 绑定按钮事件
checkPermissions() // 检查蓝牙权限
observeConnection() // 监听连接状态
loadCategory(GreetingCategory.ELDERS)
}
private fun setupChips() {
GreetingCategory.values().forEach { category ->
val chip = Chip(this).apply {
text = category.displayName
isCheckable = true
setOnClickListener { loadCategory(category) }
}
binding.chipGroup.addView(chip)
}
}
private fun loadCategory(category: GreetingCategory) {
currentCategory = category
currentTemplates = GreetingData.getByCategory(category)
currentIndex = 0
updateDisplay()
}
private fun updateDisplay() {
val template = currentTemplates[currentIndex]
binding.tvContent.text = template.content
binding.tvScene.text = "场景:${template.scene}"
binding.tvPage.text = "${currentIndex + 1}/${currentTemplates.size}"
}
// 发送祝福语到眼镜
private fun sendToGlasses() {
if (!RokidGlassesManager.isConnected) {
Toast.makeText(this, "请先连接眼镜", Toast.LENGTH_SHORT).show()
return
}
val template = currentTemplates[currentIndex]
val displayText = buildString {
appendLine("🧧 拜年祝福")
appendLine("对象:${template.category.displayName}")
appendLine()
appendLine("────── 祝福语 ──────")
appendLine()
appendLine(template.content)
appendLine()
appendLine("◀ ${currentIndex + 1}/${currentTemplates.size} ▶")
}
RokidGlassesManager.sendGreeting(displayText, object : SendCallback {
override fun onSuccess() {
runOnUiThread {
Toast.makeText(this@MainActivity, "已发送到眼镜", Toast.LENGTH_SHORT).show()
}
}
override fun onFailed(errorMsg: String) {
runOnUiThread {
Toast.makeText(this@MainActivity, errorMsg, Toast.LENGTH_SHORT).show()
}
}
})
}
}
四、踩坑实录:那些让我怀疑人生的 Bug
坑一:中文乱码
第一次发送祝福语到眼镜,显示的是一堆乱码。
原因 :String.toByteArray() 默认不是 UTF-8 编码。
scss
// ❌ 错误写法
stream = text.toByteArray()
// ✅ 正确写法
stream = text.toByteArray(Charsets.UTF_8)
坑二:快速点击翻页导致内容错乱
用户快速点击翻页按钮时,祝福语显示会出现错位。
解决方案:添加点击防抖
kotlin
private var lastClickTime = 0L
fun onNextClick() {
val now = System.currentTimeMillis()
if (now - lastClickTime < 300) return // 300ms 防抖
lastClickTime = now
// 正常翻页逻辑
}
坑三:祝福语过长被截断
眼镜屏幕有限,过长的祝福语会被截断。
解决方案:在数据层面控制祝福语长度在 80 字以内。
坑四:Android 12+ 蓝牙权限问题
Android 12 引入了新的蓝牙权限模型,需要在运行时动态申请。
kotlin
private fun checkPermissions() {
val permissions = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissions.add(Manifest.permission.BLUETOOTH_SCAN)
permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
}
// ... 申请权限
}
五、最终效果
眼镜端显示效果:
┌─────────────────────────────────┐
│ │
│ 🧧 拜年祝福 │
│ 对象:长辈 │
│ │
│ ────── 祝福语 ────── │
│ │
│ 爷爷奶奶新年好! │
│ 祝您福如东海、寿比南山! │
│ │
│ ◀ 1/6 ▶ │
│ │
└─────────────────────────────────┘
使用流程:
- 打开 APP,选择祝福对象(长辈/同辈/晚辈...)
- 浏览祝福语,或点击「随机」选择
- 点击「连接眼镜」,等待连接成功
- 点击「发送到眼镜」
- 拜年时自然抬眼,照着念就行
- (可选)点击「语音播报」,让眼镜念出来
六、总结与展望
这个项目从构思到完成花了一天时间,核心代码不到 300 行,但它解决了一个真实痛点。
技术要点回顾:
- Rokid CXR-M SDK 的两步蓝牙连接流程
controlScene+sendStream发送文本到提词器
- UTF-8 编码处理中文字符
- Material Design 3 组件构建界面
可能的后续改进:
- 自定义祝福语 - 允许用户添加自己的祝福语
- AI 生成 - 接入大模型,根据对象和场景生成个性化祝福
- 语音控制 - 用语音翻页,彻底解放双手
- 收藏功能 - 标记常用的祝福语
技术不需要多高大上,能解决生活中真实存在的问题,就是好技术。希望这个小工具能帮更多人拜年不再社死。
项目源码: NewYearGreetingHelper/
参考资料:
代码能解决 Bug,也能解决社恐。这就是技术的温度。