用 Rokid AR 眼镜打造沉浸式外语学习助手:从想法到落地的完整开发实录

用 Rokid AR 眼镜打造沉浸式外语学习助手:从想法到落地的完整开发实录

一、缘起:碎片化学习的困境与AR的可能性

学习者的日常困境

作为一名外语学习者,我常常陷入这样的困境:地铁上背单词,频繁拿出手机解锁、打开APP、翻看释义,整个过程被各种通知打断;跑步时想练习听力,却因频繁操作手机而中断运动节奏;睡前想复习今日所学,拿起手机后却被社交软件吸引走了注意力。

这些问题的本质是:传统学习方式需要用户主动且频繁地与设备交互,这种交互本身成为了一种负担,也成为了分心的入口。

为什么是AR眼镜?

当Rokid AR眼镜进入我的视野时,一个想法逐渐清晰:如果学习内容可以始终"悬浮"在我眼前,需要时抬眼即见,不需要时也不会干扰正常生活,这或许能从根本上改变学习体验。

AR眼镜相比手机有三个独特优势:

  1. 零干扰:眼镜上没有社交软件、没有消息推送,是一个纯净的学习空间

  2. 即时性:抬眼即见,无需掏手机、解锁、找APP

  3. 私密性:只有佩戴者能看到内容,公共场所也能安心学习

选型:CXR-M SDK

Rokid 提供了 CXR-M SDK,专门为提词器(Word Tips)场景设计,这正是我们需要的------将文本内容推送到眼镜显示。SDK 还内置了 TTS(文字转语音)功能,可以解决发音学习的问题。

经过评估,CXR-M SDK 的以下特性使其成为理想选择:

特性 说明
WORD_TIPS 场景 专为文本显示优化
蓝牙直连 无需额外硬件
TTS 集成 支持语音播报
纯 Android 开发 学习成本低

二、系统设计:构建学习闭环

核心设计理念

我们的目标是构建一个「学习闭环」:

  • 认知:看到英文单词/句子

  • 测试:主动回忆中文含义

  • 反馈:揭示正确答案

  • 巩固:通过 TTS 听发音加深记忆

系统架构

整个系统采用「手机控制端 + 眼镜显示端」的双端协作模式:

为什么用手机做控制端?

  1. 眼镜没有触控输入设备,需要外部控制源

  2. 手机便于展示完整的学习内容和操作按钮

  3. 手机可以承担数据存储和业务逻辑

三种学习模式

为了满足不同学习场景,设计了三种模式:

  1. 单词记忆:显示单词、音标,点击显示中文释义

  2. 句子跟读:显示完整句子,配合 TTS 朗读

  3. 听写练习:先播放 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_CONNECTBLUETOOTH_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
    }
}

设计要点解析

  1. 单例模式:全局只有一个 CxrApi 实例,避免重复初始化

  2. 回调封装:将 SDK 复杂的回调包装成简洁的接口

  3. 双重连接initBluetooth 获取连接参数后,再调用 connectBluetooth 完成连接

  4. 场景控制:发送内容前必须先激活对应场景

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(),导致连接未真正建立就尝试发送内容。

解决 :必须等待第二次回调,确认 connectBluetoothonConnected 被触发后才算真正连接成功。

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/                   │
│                               │
│  中文:放弃                    │
└──────────────────────────────┘

完整使用流程

  1. 准备阶段:手机蓝牙开启,眼镜已配对

  2. 连接眼镜:点击「连接眼镜」按钮

  3. 选择模式:单词记忆 / 句子跟读 / 听写练习

  4. 开始学习

  • 浏览英文内容,主动回忆含义

  • 点击「显示答案」核对

  • 点击「语音播报」听发音

  • 点击「发送到眼镜」同步内容

  1. 翻页继续:下一项学习内容

功能清单

功能 实现方式 价值
三种学习模式 LearnMode 枚举 + 数据过滤 适配不同学习场景
答案隐藏 showAnswer 状态控制 促进主动回忆
眼镜同步 WORD_TIPS 场景 随时随地复习
TTS 播报 sendTtsContent() 学习正确发音
音标显示 数据模型 phonetic 字段 辅助发音理解
蓝牙连接 CXR-M SDK 异步连接 无线便捷使用

六、不足与未来展望

当前版本的限制

  1. 内容有限:目前仅内置少量示例数据

  2. 无学习记录:没有进度跟踪和统计

  3. 无生词本:不能标记重点词汇

  4. 单一语言:仅支持英语学习

后续优化方向

短期计划

  • 接入本地词库,支持自定义学习内容

  • 添加学习进度统计和打卡功能

  • 实现生词本和复习提醒

长期愿景

  • 集成 AI 对话功能,实现口语练习

  • 加入语音识别,实现口语评测

  • 支持多语种学习(日语、韩语等)

  • 云端同步学习数据


七、总结

这个项目让我深刻体会到 AR 眼镜在教育领域的潜力。与传统学习方式相比,AR 眼镜创造了一个专注、即时、沉浸的学习环境。

技术层面,CXR-M SDK 提供了简洁的 API,核心代码不超过 200 行就实现了眼镜通信功能。蓝牙连接、场景控制、内容发送、TTS 播放,每个环节都有清晰的接口设计。

产品层面,「主动回忆」的学习理念通过答案隐藏功能得以实现,「碎片时间利用」通过眼镜的即时显示特性成为可能。

AR 眼镜还是一片待开垦的沃土,期待更多开发者加入,探索 AR + 教育的无限可能。


项目源码LanguageHelper/

参考资料

相关推荐
tzc_fly2 小时前
VideoWorld1-2:纯视频学习获取世界知识
学习·音视频
降临-max2 小时前
JavaWeb企业级开发---Maven高级
java·笔记·学习·maven
Gorgous—l2 小时前
数据结构算法学习:LeetCode热题100-贪心算法篇(数组中的第K个最大元素、 前 K 个高频元素、数据流的中位数)
数据结构·学习·算法
哎呦 你干嘛~3 小时前
tcpip通讯
学习
Dxy12393102163 小时前
PyTorch训练的艺术:精通ReduceLROnPlateau学习率调度器
人工智能·pytorch·学习
炽烈小老头3 小时前
【 每天学习一点算法 2026/03/11】从前序与中序遍历序列构造二叉树
学习·算法
敲代码的嘎仔3 小时前
Java后端开发——基础面试题汇总
java·开发语言·笔记·后端·学习·spring·中间件
大卡拉米3 小时前
ClaudeCode安装及使用
前端·学习
xhyyvr3 小时前
VR应急救护学习机|沉浸式体验VR应急救护
学习·vr