用 Rokid AR 眼镜打造沉浸式外语学习助手从想法到落地的完整开发实录一、缘起碎片化学习的困境与AR的可能性学习者的日常困境作为一名外语学习者我常常陷入这样的困境地铁上背单词频繁拿出手机解锁、打开APP、翻看释义整个过程被各种通知打断跑步时想练习听力却因频繁操作手机而中断运动节奏睡前想复习今日所学拿起手机后却被社交软件吸引走了注意力。这些问题的本质是传统学习方式需要用户主动且频繁地与设备交互这种交互本身成为了一种负担也成为了分心的入口。为什么是AR眼镜当Rokid AR眼镜进入我的视野时一个想法逐渐清晰如果学习内容可以始终悬浮在我眼前需要时抬眼即见不需要时也不会干扰正常生活这或许能从根本上改变学习体验。AR眼镜相比手机有三个独特优势零干扰眼镜上没有社交软件、没有消息推送是一个纯净的学习空间即时性抬眼即见无需掏手机、解锁、找APP私密性只有佩戴者能看到内容公共场所也能安心学习选型CXR-M SDKRokid 提供了 CXR-M SDK专门为提词器Word Tips场景设计这正是我们需要的——将文本内容推送到眼镜显示。SDK 还内置了 TTS文字转语音功能可以解决发音学习的问题。经过评估CXR-M SDK 的以下特性使其成为理想选择特性说明WORD_TIPS 场景专为文本显示优化蓝牙直连无需额外硬件TTS 集成支持语音播报纯 Android 开发学习成本低二、系统设计构建学习闭环核心设计理念我们的目标是构建一个「学习闭环」认知看到英文单词/句子测试主动回忆中文含义反馈揭示正确答案巩固通过 TTS 听发音加深记忆系统架构整个系统采用「手机控制端 眼镜显示端」的双端协作模式为什么用手机做控制端眼镜没有触控输入设备需要外部控制源手机便于展示完整的学习内容和操作按钮手机可以承担数据存储和业务逻辑三种学习模式为了满足不同学习场景设计了三种模式单词记忆显示单词、音标点击显示中文释义句子跟读显示完整句子配合 TTS 朗读听写练习先播放 TTS再显示文本供核对三、开发实战从零开始构建应用3.1 项目环境配置首先创建一个标准的 Android 项目配置 CXR-M SDK 依赖// settings.gradle.kts - 添加 Rokid Maven 仓库 dependencyResolutionManagement { repositories { maven { url uri(https://maven.rokid.com/repository/maven-public/) } google() mavenCentral() } }// app/build.gradle.kts - 添加 SDK 依赖 android { namespace com.rokid.language compileSdk 34 defaultConfig { applicationId com.rokid.languagehelper minSdk 28 targetSdk 34 } compileOptions { sourceCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget 17 } } dependencies { // 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) implementation(androidx.constraintlayout:constraintlayout:2.1.4) }权限配置同样重要蓝牙连接需要声明相关权限!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN /注意Android 12 需要动态申请BLUETOOTH_CONNECT和BLUETOOTH_SCAN权限。3.2 数据模型设计良好的数据模型是应用的基石。我们定义了学习模式和内容模型// LearningContent.kt /** * 学习模式枚举 * 每种模式对应不同的学习场景和展示方式 */ enum class LearnMode(val displayName: String) { VOCABULARY(单词记忆), // 单词 → 音标 → 释义 SENTENCE(句子跟读), // 句子 → 翻译 → 跟读 DICTATION(听写) // 语音 → 拼写 → 核对 } /** * 学习内容数据模型 * * property id 唯一标识 * property mode 所属学习模式 * property english 英文内容 * property chinese 中文翻译 * property phonetic 音标可选单词模式使用 * property difficulty 难度等级 1-5 */ data class LearningContent( val id: Int, val mode: LearnMode, val english: String, val chinese: String, val phonetic: String? null, val difficulty: Int 1 )数据层使用单例对象管理预设内容便于后续扩展// LearningContent.kt (续) object LearningData { private val _contents listOf( // 单词记忆 - 基础词汇 LearningContent(1, LearnMode.VOCABULARY, abandon, 放弃, /əˈbændən/, 2), LearningContent(2, LearnMode.VOCABULARY, beautiful, 美丽的, /ˈbjuːtɪfl/, 1), LearningContent(3, LearnMode.VOCABULARY, celebrate, 庆祝, /ˈselɪbreɪt/, 2), // 句子跟读 - 日常对话 LearningContent(4, LearnMode.SENTENCE, How are you?, 你好吗, null, 1), LearningContent(5, LearnMode.SENTENCE, Nice to meet you., 很高兴见到你。, null, 1), LearningContent(6, LearnMode.SENTENCE, What time is it?, 几点了, null, 1), // 听写练习 - 简单词汇 LearningContent(7, LearnMode.DICTATION, apple, 苹果, /ˈæpl/, 1), LearningContent(8, LearnMode.DICTATION, banana, 香蕉, /bəˈnænə/, 1), LearningContent(9, LearnMode.DICTATION, orange, 橙子, /ˈɔrɪndʒ/, 1) ) /** 获取全部内容 */ fun getContents(): ListLearningContent _contents /** 按模式筛选内容 */ fun getByMode(mode: LearnMode): ListLearningContent _contents.filter { it.mode mode } }3.3 SDK 封装层RokidGlassesManager这是整个应用的核心负责与眼镜的通信。我们将其封装为一个单例对象提供简洁的 API// RokidGlassesManager.kt package com.rokid.language.sdk import android.Manifest import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.content.Context import android.content.pm.PackageManager import androidx.core.app.ActivityCompat import com.rokid.cxr.api.CxrApi import com.rokid.cxr.callback.BluetoothStatusCallback import com.rokid.cxr.util.ValueUtil /** * Rokid 眼镜通信管理器 * * 封装 CXR-M SDK提供眼镜连接、内容发送、TTS 播放等功能 */ object RokidGlassesManager { private val cxrApi: CxrApi by lazy { CxrApi.getInstance() } private var connectionCallback: ConnectionCallback? null // 回调接口定义 interface ConnectionCallback { fun onConnecting() fun onConnected() fun onDisconnected() fun onFailed(errorMsg: String) } interface SendCallback { fun onSuccess() fun onFailed(errorMsg: String) } // 连接状态 /** 当前是否已连接眼镜 */ val isConnected: Boolean get() cxrApi.isBluetoothConnected fun setConnectionCallback(callback: ConnectionCallback?) { this.connectionCallback callback } // 设备发现 /** * 从已配对设备中查找 Rokid 眼镜 */ fun findRokidGlasses(bluetoothAdapter: BluetoothAdapter): BluetoothDevice? { if (ActivityCompat.checkSelfPermission( bluetoothAdapter.javaClass, Manifest.permission.BLUETOOTH_CONNECT ) ! PackageManager.PERMISSION_GRANTED ) return null return bluetoothAdapter.bondedDevices.find { it.name?.contains(Rokid, ignoreCase true) || it.name?.contains(Glasses, ignoreCase true) } } // 连接管理 /** * 连接眼镜设备 * * 连接流程initBluetooth → 获取连接信息 → connectBluetooth */ fun connectGlasses(context: Context, device: BluetoothDevice) { connectionCallback?.onConnecting() cxrApi.initBluetooth(context, device, object : BluetoothStatusCallback() { override fun onConnectionInfo( uuid: String?, mac: String?, account: String?, type: Int ) { if (!uuid.isNullOrEmpty() !mac.isNullOrEmpty()) { // 获取到连接信息发起实际连接 cxrApi.connectBluetooth( context, uuid, mac, object : BluetoothStatusCallback() { override fun onConnected() { connectionCallback?.onConnected() } override fun onDisconnected() { connectionCallback?.onDisconnected() } override fun onFailed(e: ValueUtil.CxrBluetoothErrorCode?) { connectionCallback?.onFailed(e?.name ?: 连接失败) } override fun onConnectionInfo( a: String?, b: String?, c: String?, d: Int ) { /* 二次回调忽略 */ } } ) } else { connectionCallback?.onFailed(获取连接信息失败) } } override fun onConnected() { connectionCallback?.onConnected() } override fun onDisconnected() { connectionCallback?.onDisconnected() } override fun onFailed(e: ValueUtil.CxrBluetoothErrorCode?) { connectionCallback?.onFailed(e?.name ?: 连接失败) } }) } fun disconnect() { cxrApi.deinitBluetooth() } // 内容发送 /** * 发送文本内容到眼镜显示 * * param text 要显示的文本 * param callback 发送结果回调 */ fun sendContent(text: String, callback: SendCallback? null): Boolean { if (!isConnected) { callback?.onFailed(眼镜未连接) return false } // 1. 激活 WORD_TIPS 场景 cxrApi.controlScene(ValueUtil.CxrSceneType.WORD_TIPS, true, null) // 2. 发送文本流 val status cxrApi.sendStream( type ValueUtil.CxrStreamType.WORD_TIPS, stream text.toByteArray(Charsets.UTF_8), fileName learning.txt, cb object : com.rokid.cxr.callback.SendStatusCallback() { override fun onSendSucceed() { callback?.onSuccess() } override fun onSendFailed(e: ValueUtil.CxrSendErrorCode?) { callback?.onFailed(e?.name ?: 发送失败) } } ) return status ValueUtil.CxrStatus.REQUEST_SUCCEED } /** * 发送 TTS 语音播报 * * param text 要朗读的文本 * return 是否发送成功 */ 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 } }设计要点解析单例模式全局只有一个 CxrApi 实例避免重复初始化回调封装将 SDK 复杂的回调包装成简洁的接口双重连接initBluetooth获取连接参数后再调用connectBluetooth完成连接场景控制发送内容前必须先激活对应场景3.4 主界面实现MainActivity主界面是用户交互的核心负责学习模式切换、内容展示、眼镜通信等功能// MainActivity.kt package com.rokid.language class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private var contents: ListLearningContent emptyList() private var currentIndex 0 private var showAnswer false private var currentMode: LearnMode LearnMode.VOCABULARY override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.title 外语学习助手 // 初始化 contents LearningData.getContents() setupButtons() checkPermissions() observeConnection() updateDisplay() } // 当前模式下的内容列表 private val currentContents: ListLearningContent get() contents.filter { it.mode currentMode }按钮事件绑定private fun setupButtons() { // 模式切换按钮 binding.btnVocabulary.setOnClickListener { setMode(LearnMode.VOCABULARY) } binding.btnSentence.setOnClickListener { setMode(LearnMode.SENTENCE) } binding.btnDictation.setOnClickListener { setMode(LearnMode.DICTATION) } // 翻页按钮 binding.btnPrev.setOnClickListener { if (currentIndex 0) { currentIndex-- showAnswer false updateDisplay() } } binding.btnNext.setOnClickListener { if (currentIndex currentContents.size - 1) { currentIndex showAnswer false updateDisplay() } } // 显示答案按钮 - 核心交互逻辑 binding.btnShowAnswer.setOnClickListener { showAnswer true updateDisplay() } // 眼镜连接/断开 binding.btnConnect.setOnClickListener { if (RokidGlassesManager.isConnected) { RokidGlassesManager.disconnect() } else { connectGlasses() } } // 发送内容到眼镜 binding.btnSend.setOnClickListener { sendToGlasses() } // TTS 语音播报 binding.btnTts.setOnClickListener { sendTts() } }内容发送到眼镜的关键逻辑private fun sendToGlasses() { if (!RokidGlassesManager.isConnected) { Toast.makeText(this, 请先连接眼镜, Toast.LENGTH_SHORT).show() return } val content currentContents.getOrNull(currentIndex) ?: return val text buildDisplayText(content) RokidGlassesManager.sendContent(text, object : RokidGlassesManager.SendCallback { override fun onSuccess() { runOnUiThread { Toast.makeText(thisMainActivity, 已发送到眼镜, Toast.LENGTH_SHORT).show() } } override fun onFailed(errorMsg: String) { runOnUiThread { Toast.makeText(thisMainActivity, errorMsg, Toast.LENGTH_SHORT).show() } } }) } /** * 构建眼镜端显示的文本 * * 格式化学习内容使其在眼镜上清晰易读 */ private fun buildDisplayText(content: LearningContent): String buildString { // 模式标题 appendLine( ${currentMode.displayName}) appendLine() // 进度指示 appendLine(────── 内容 ${currentIndex 1}/${currentContents.size} ──────) appendLine() // 英文内容 appendLine(content.english) // 音标如果有 content.phonetic?.let { appendLine(it) } appendLine() // 答案区域 if (showAnswer) { appendLine(中文${content.chinese}) } else { appendLine( 手机点击显示答案) } }连接状态监听private fun observeConnection() { RokidGlassesManager.setConnectionCallback( object : RokidGlassesManager.ConnectionCallback { override fun onConnecting() { runOnUiThread { binding.btnConnect.text 连接中... } } override fun onConnected() { runOnUiThread { binding.btnConnect.text 断开连接 Toast.makeText(thisMainActivity, 眼镜已连接, Toast.LENGTH_SHORT).show() } } override fun onDisconnected() { runOnUiThread { binding.btnConnect.text 连接眼镜 } } override fun onFailed(errorMsg: String) { runOnUiThread { binding.btnConnect.text 连接眼镜 Toast.makeText(thisMainActivity, errorMsg, Toast.LENGTH_SHORT).show() } } } ) }四、关键技术点与踩坑经验4.1 蓝牙连接的异步处理CXR-M SDK 的蓝牙连接是异步操作涉及两次回调initBluetooth → onConnectionInfo(uuid, mac) → connectBluetooth → onConnected踩坑最初我在onConnectionInfo中直接调用onConnected()导致连接未真正建立就尝试发送内容。解决必须等待第二次回调确认connectBluetooth的onConnected被触发后才算真正连接成功。4.2 内容格式优化眼镜显示区域有限长句子需要换行处理/** * 智能换行按指定长度分割文本 */ fun formatText(text: String, maxLineLength: Int 25): String { if (text.length maxLineLength) return text val words text.split( ) val lines mutableListOfString() var currentLine for (word in words) { if (currentLine.length word.length 1 maxLineLength) { currentLine if (currentLine.isEmpty()) word else $word } else { if (currentLine.isNotEmpty()) lines.add(currentLine) currentLine word } } if (currentLine.isNotEmpty()) lines.add(currentLine) return lines.joinToString(\n) }4.3 答案隐藏的交互设计主动回忆Active Recall是高效学习的关键。我们通过showAnswer状态变量实现// 默认隐藏答案 private var showAnswer false // 切换内容时重置状态 binding.btnNext.setOnClickListener { currentIndex showAnswer false // 重置为隐藏 updateDisplay() } // 用户主动点击才显示 binding.btnShowAnswer.setOnClickListener { showAnswer true updateDisplay() }4.4 权限处理的版本兼容Android 12 引入了新的蓝牙权限模型private fun checkPermissions() { val permissions mutableListOfString() // Android 12 需要新权限 if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { permissions.add(Manifest.permission.BLUETOOTH_SCAN) permissions.add(Manifest.permission.BLUETOOTH_CONNECT) } val notGranted permissions.filter { ContextCompat.checkSelfPermission(this, it) ! PackageManager.PERMISSION_GRANTED } if (notGranted.isNotEmpty()) { ActivityCompat.requestPermissions(this, notGranted.toTypedArray(), 100) } }五、效果展示与使用流程眼镜端显示效果隐藏答案时┌──────────────────────────────┐ │ 单词记忆 │ │ │ │ ────── 内容 1/20 ────── │ │ │ │ abandon │ │ /əˈbændən/ │ │ │ │ 手机点击显示答案 │ └──────────────────────────────┘显示答案后┌──────────────────────────────┐ │ 单词记忆 │ │ │ │ ────── 内容 1/20 ────── │ │ │ │ abandon │ │ /əˈbændən/ │ │ │ │ 中文放弃 │ └──────────────────────────────┘完整使用流程准备阶段手机蓝牙开启眼镜已配对连接眼镜点击「连接眼镜」按钮选择模式单词记忆 / 句子跟读 / 听写练习开始学习浏览英文内容主动回忆含义点击「显示答案」核对点击「语音播报」听发音点击「发送到眼镜」同步内容翻页继续下一项学习内容功能清单功能实现方式价值三种学习模式LearnMode枚举 数据过滤适配不同学习场景答案隐藏showAnswer状态控制促进主动回忆眼镜同步WORD_TIPS场景随时随地复习TTS 播报sendTtsContent()学习正确发音音标显示数据模型phonetic字段辅助发音理解蓝牙连接CXR-M SDK 异步连接无线便捷使用六、不足与未来展望当前版本的限制内容有限目前仅内置少量示例数据无学习记录没有进度跟踪和统计无生词本不能标记重点词汇单一语言仅支持英语学习后续优化方向短期计划接入本地词库支持自定义学习内容添加学习进度统计和打卡功能实现生词本和复习提醒长期愿景集成 AI 对话功能实现口语练习加入语音识别实现口语评测支持多语种学习日语、韩语等云端同步学习数据七、总结这个项目让我深刻体会到 AR 眼镜在教育领域的潜力。与传统学习方式相比AR 眼镜创造了一个专注、即时、沉浸的学习环境。技术层面CXR-M SDK 提供了简洁的 API核心代码不超过 200 行就实现了眼镜通信功能。蓝牙连接、场景控制、内容发送、TTS 播放每个环节都有清晰的接口设计。产品层面「主动回忆」的学习理念通过答案隐藏功能得以实现「碎片时间利用」通过眼镜的即时显示特性成为可能。AR 眼镜还是一片待开垦的沃土期待更多开发者加入探索 AR 教育的无限可能。项目源码LanguageHelper/参考资料CXR-M SDK 官方文档Rokid 开发者论坛