用 Rokid AR 眼镜打造沉浸式外语学习助手:从想法到落地的完整开发实录
一、缘起:碎片化学习的困境与AR的可能性
学习者的日常困境
作为一名外语学习者,我常常陷入这样的困境:地铁上背单词,频繁拿出手机解锁、打开APP、翻看释义,整个过程被各种通知打断;跑步时想练习听力,却因频繁操作手机而中断运动节奏;睡前想复习今日所学,拿起手机后却被社交软件吸引走了注意力。
这些问题的本质是:传统学习方式需要用户主动且频繁地与设备交互,这种交互本身成为了一种负担,也成为了分心的入口。
为什么是AR眼镜?
当Rokid AR眼镜进入我的视野时,一个想法逐渐清晰:如果学习内容可以始终"悬浮"在我眼前,需要时抬眼即见,不需要时也不会干扰正常生活,这或许能从根本上改变学习体验。
AR眼镜相比手机有三个独特优势:
-
零干扰:眼镜上没有社交软件、没有消息推送,是一个纯净的学习空间
-
即时性:抬眼即见,无需掏手机、解锁、找APP
-
私密性:只有佩戴者能看到内容,公共场所也能安心学习
选型:CXR-M SDK
Rokid 提供了 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:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.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(): List<LearningContent> = _contents
/** 按模式筛选内容 */
fun getByMode(mode: LearnMode): List<LearningContent> =
_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: List<LearningContent> = 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: List<LearningContent>
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(this@MainActivity, "已发送到眼镜", Toast.LENGTH_SHORT).show()
}
}
override fun onFailed(errorMsg: String) {
runOnUiThread {
Toast.makeText(this@MainActivity, 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(this@MainActivity, "眼镜已连接", Toast.LENGTH_SHORT).show()
}
}
override fun onDisconnected() {
runOnUiThread { binding.btnConnect.text = "连接眼镜" }
}
override fun onFailed(errorMsg: String) {
runOnUiThread {
binding.btnConnect.text = "连接眼镜"
Toast.makeText(this@MainActivity, 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 = mutableListOf<String>()
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 = mutableListOf<String>()
// 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/
参考资料: