1. 六代希沃一体机侧边栏改造的背景与核心思路
1.1 为什么要在侧边栏做随机抽选
六代希沃一体机在教室里的使用频率极高,老师日常上课最常用的几个动作无非是:打开白板、切换课件、调用班级优化大师、随机点人回答问题。前三个动作系统都给了比较顺手的入口,唯独“随机抽选”这个高频需求,在六代机的侧边栏里默认是没有的。很多老师要么退出去开班级优化大师,要么用第三方小工具,操作链路一长,课堂节奏就被打断了。
我所在的学校去年批量换装了六代机,老师们反馈最多的就是“能不能在侧边栏直接点一下就能抽人”。这个需求听起来小,但它是真实发生在每一节课上的。侧边栏本身就是希沃给老师预留的快捷操作区,把随机抽选塞进去,等于把最高频的课堂互动动作压缩到一次点击,这个改造的价值就在这里。
需要说明的是,六代希沃一体机运行的是基于 Android 深度定制的教学系统,侧边栏本质上是系统层的一个悬浮窗服务,它并不是一个完全开放的插件市场。所以“添加功能”这件事,本质上是在系统允许的范围内,通过侧边栏的自定义入口去挂载一个我们自己的抽选应用或网页。这一点想清楚了,后面的方案选型才不会走偏。
1.2 三种可行方案与选型逻辑
在动手之前,我把能走的路都捋了一遍,大致有三种:
- 方案A:利用侧边栏已有的自定义应用入口。六代机的侧边栏设置里,部分系统版本允许添加“自定义应用”快捷方式。把抽选程序打包成一个 APK 装到系统里,再把它挂到侧边栏。优点是体验最原生,点一下直接拉起;缺点是不同批次的六代机系统版本差异较大,有的版本这个入口被隐藏了。
- 方案B:用侧边栏的网页快捷方式挂本地 HTML。把抽选逻辑写成一个单文件 HTML,放到本地存储,通过侧边栏的浏览器快捷方式打开。优点是零安装、跨版本兼容性好;缺点是首次打开有加载感,且需要处理本地文件的访问权限。
- 方案C:借助第三方侧边栏工具做中转。在系统里装一个支持自定义快捷方式的悬浮侧边栏工具,把抽选功能挂到它上面。优点是灵活;缺点是会和系统原生侧边栏产生视觉和操作上的冲突,老师容易点错。
我最终选的是方案A为主、方案B为备的组合策略。原因很直接:六代机的原生侧边栏在视觉层级上是最高的,老师已经形成了肌肉记忆,任何额外的悬浮工具都会造成干扰。方案A能跑通就优先用A,跑不通的机器退回B,保证每个班都能用上。这个取舍的核心逻辑是:课堂工具的稳定性优先级高于功能的丰富度,宁可少一个动画效果,也不能出现点不开的情况。
1.3 改造前必须确认的三件事
动手之前有三件事必须先确认,否则后面全是返工:
第一,确认机器的系统版本和侧边栏版本。在“设置-关于本机”里能看到 Android 版本号和希沃系统版本号。六代机常见的系统版本跨度比较大,侧边栏的设置项位置和可添加的应用类型都不一样。我建议先把版本号记下来,后面排查问题时这是第一手信息。
第二,确认是否开启了未知来源应用安装权限。要装自己打包的 APK,这个权限是前提。部分学校统一管控的机器这个权限被锁了,那就只能走方案B。
第三,确认侧边栏是否处于可编辑状态。有的机器侧边栏被管理员锁定,长按不出现编辑按钮。这种情况需要先联系设备管理员解锁,不要硬试,硬试容易触发系统的保护机制。
提示:改造前建议先用U盘把原始侧边栏的配置截图保存一份,万一改乱了可以对照恢复。这个习惯我养成了很多年,救过好几次场。
2. 随机抽选功能的核心细节与实操要点
2.1 抽选逻辑到底该怎么设计
很多人一上来就想着写个随机数,但课堂抽选和普通的随机数生成是两回事。我踩过的坑是:第一版直接用Math.random()抽,结果连续三次抽到同一个学生,老师当场就尴尬了。课堂场景对“随机”的期待其实是均匀且不重复,而不是数学意义上的纯随机。
所以抽选逻辑我做了三层设计:
- 去重层:维护一个已抽名单,抽过的人在当前轮次内不再出现,直到所有人都抽过一遍才重置。这解决了连续抽到同一个人的问题。
- 权重层:支持给个别学生设置权重,比如某个学生最近状态不好,老师想多给他机会,可以调高权重。这个功能不是必须的,但加上之后老师会觉得“懂我”。
- 展示层:抽选过程要有短暂的滚动动画,最后定格。纯瞬间出结果会让课堂缺少那个“悬念感”,滚动动画本身就是课堂互动的一部分。
数据结构上,我用一个数组存名单,一个 Set 存已抽记录,一个 Map 存权重。抽选时先过滤掉已抽的,再按权重做加权随机。这个逻辑不复杂,但每一步都对应着真实的课堂需求。
2.2 名单数据的存储与读取
名单不能每次打开都重新输入,必须持久化。六代机的本地存储有几个选择:localStorage、IndexedDB、以及写到应用私有目录的文件。我选的是localStorage,原因是抽选程序本身很轻,名单数据量小(一个班最多几十人),localStorage 的 5MB 容量绰绰有余,而且读写是同步的,代码简单不容易出 bug。
存储结构我设计成这样:
{ "className": "高二(3)班", "students": ["张三", "李四", "王五"], "drawn": ["张三"], "weights": {"李四": 2}, "lastUpdated": "2025-01-15T10:30:00" }这里有个细节要注意:drawn数组在每次抽选后要同步更新,并且要在页面关闭前确保写入。我遇到过页面被系统直接杀掉导致数据没存上的情况,后来改成每次抽选后立即写入,问题就没了。虽然多了一点 IO,但数据安全比性能重要。
另外,名单导入我做了两种方式:手动逐个添加,以及粘贴批量导入。批量导入用换行或逗号分隔都支持,老师从 Excel 里复制一列名字直接粘进来就能用。这个细节看着小,但省了老师大量输入时间。
2.3 侧边栏挂载的关键操作
把抽选程序挂到侧边栏,是整个改造里最容易卡住的一步。我分两种情况说:
情况一:系统侧边栏支持自定义应用。进入侧边栏编辑模式,找到“添加应用”或“自定义快捷方式”,从已安装应用列表里选中我们的抽选 APK。这里有个坑:部分系统版本只显示系统预装应用,不显示第三方应用。遇到这种情况,需要先把 APK 安装到系统应用目录(需要 root 或系统签名),普通安装方式挂不上去。如果学校机器没有 root,这条路就走不通,直接转方案B。
情况二:系统侧边栏只支持网页快捷方式。把抽选 HTML 放到/sdcard/xuanji/index.html,然后在侧边栏添加网页快捷方式,地址填file:///sdcard/xuanji/index.html。这里要注意 Android 的文件访问权限,部分系统版本对file://协议有限制,需要改用content://或者把文件放到应用可访问的目录。我实测下来,放到应用私有目录再通过 WebView 加载是最稳的。
注意:不要试图去修改系统侧边栏的 APK 文件,六代机有系统完整性校验,改了大概率开不了机。这个坑我替你们踩过了,代价是一台机器返厂。
2.4 界面适配与触控优化
一体机的屏幕大,老师站在屏幕前操作,手指点击的精度远不如手机。所以抽选界面的按钮必须做大,最小触控区域我建议不小于 80px。字体也要大,保证后排学生也能看清抽选结果。
配色上我用的是高对比度方案:深色背景配亮色文字,抽选结果用大号字体居中显示。教室光线复杂,浅色背景在强光下反光严重,深色背景更稳。这个细节是实际用下来才体会到的,第一版用白色背景,下午阳光一照,屏幕上的字基本看不清。
另外,抽选按钮我做了防误触处理:点击后 500ms 内不接受第二次点击。老师有时候手快会连点,没有防抖的话会连续抽两次,结果就乱了。
3. 完整实操流程与核心环节实现
3.1 开发环境与工具准备
抽选程序我选择用纯前端技术栈写,HTML + CSS + JavaScript,不依赖任何框架。原因很简单:一体机上的 WebView 版本参差不齐,用框架容易遇到兼容性问题,纯原生写法最稳。开发工具用 VS Code,调试用 Chrome 的远程调试功能连到一体机上。
如果你要打包成 APK,需要一个简单的 WebView 壳。我用的是 Android Studio 建一个空项目,主 Activity 里放一个全屏 WebView 加载本地 HTML。这个壳代码量很少,核心就是几行配置。打包时注意 minSdkVersion 要覆盖六代机的系统版本,我设的是 API 24,实测覆盖了绝大多数机器。
文件目录结构我这样组织:
xuanji/ ├── index.html # 主界面 ├── style.css # 样式 ├── app.js # 抽选逻辑 ├── data.js # 名单数据管理 └── assets/ # 图标等资源3.2 抽选核心代码实现
抽选的核心逻辑我单独放在app.js里,关键函数是pickStudent()。先看加权随机的实现:
function weightedRandom(students, weights) { // 计算总权重 let totalWeight = 0; students.forEach(name => { totalWeight += (weights[name] || 1); }); // 生成随机点 let random = Math.random() * totalWeight; // 找到对应的学生 for (let name of students) { random -= (weights[name] || 1); if (random <= 0) { return name; } } return students[students.length - 1]; }这个加权随机的思路是:把每个学生的权重想象成一段线段的长度,权重越大线段越长,随机点落在这段线段上的概率就越大。这是加权抽样的标准做法,比“按权重复制多份再随机”要高效得多。
然后是去重逻辑:
function pickStudent() { // 获取未抽过的学生 let available = students.filter(name => !drawn.includes(name)); // 如果都抽过了,重置 if (available.length === 0) { drawn = []; available = [...students]; } // 加权随机 let picked = weightedRandom(available, weights); // 记录已抽 drawn.push(picked); saveData(); return picked; }这里有个边界情况要处理:如果名单里只有一个学生,available长度为 1,加权随机直接返回这个学生,逻辑是对的。但如果名单为空,要给出提示而不是报错。我在调用pickStudent()之前加了一个空名单检查。
3.3 滚动动画的实现细节
滚动动画我用的是requestAnimationFrame做逐帧更新,而不是 CSS 动画。原因是 CSS 动画的时长和缓动曲线不好精确控制,而课堂场景需要动画时长刚好(我设的是 1.5 秒),太快没悬念,太慢老师等得着急。
动画逻辑是这样的:先快速滚动显示随机名字,速度逐渐减慢,最后定格在抽选结果上。关键代码如下:
function rollAnimation(finalResult, callback) { const duration = 1500; // 总时长1.5秒 const startTime = performance.now(); function frame(currentTime) { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); // 缓动函数:先快后慢 const eased = 1 - Math.pow(1 - progress, 3); if (progress < 1) { // 滚动中,显示随机名字 const randomName = students[Math.floor(Math.random() * students.length)]; displayName(randomName); requestAnimationFrame(frame); } else { // 结束,显示最终结果 displayName(finalResult); callback && callback(); } } requestAnimationFrame(frame); }缓动函数1 - Math.pow(1 - progress, 3)是三次缓出,效果是开始快、结尾慢,视觉上很自然。这个函数我调了好几次参数,二次缓出结尾还是有点急,三次刚好。
3.4 名单管理界面的实现
名单管理我做成一个可折叠的面板,默认收起,点击“管理名单”才展开。这样主界面保持干净,老师上课时不会被管理功能干扰。
管理面板里支持:添加单个学生、批量粘贴导入、删除学生、清空名单、重置已抽记录。批量导入的解析逻辑要处理多种分隔符:
function parseNames(input) { // 支持换行、逗号、顿号、空格分隔 return input .split(/[\n,,、\s]+/) .map(name => name.trim()) .filter(name => name.length > 0); }这个正则[\n,,、\s]+覆盖了换行、英文逗号、中文逗号、顿号、空白字符。老师从 Excel 复制过来的名字通常是换行分隔的,从 Word 复制可能是逗号或顿号,这个正则都能处理。
导入后要去重,避免同一个名字出现两次。去重我用 Set:
const uniqueNames = [...new Set(parsedNames)];3.5 部署到一体机的完整步骤
部署流程我整理成了一张表,按顺序操作就行:
| 步骤 | 操作内容 | 注意事项 |
|---|---|---|
| 1 | 开启未知来源安装权限 | 设置-安全-未知来源 |
| 2 | 安装抽选 APK | 用U盘拷贝安装包 |
| 3 | 打开抽选程序测试 | 确认名单功能正常 |
| 4 | 进入侧边栏编辑模式 | 长按侧边栏空白处 |
| 5 | 添加自定义应用 | 从应用列表选抽选程序 |
| 6 | 调整侧边栏图标位置 | 拖到顺手的位置 |
| 7 | 退出编辑模式测试 | 点击图标确认能拉起 |
如果第5步找不到抽选程序,说明系统侧边栏不显示第三方应用,转方案B:把 HTML 文件放到/sdcard/xuanji/,侧边栏添加网页快捷方式,地址填file:///sdcard/xuanji/index.html。
提示:部署完成后建议重启一次机器,确认侧边栏配置持久化生效。有的机器不重启的话侧边栏配置不会写入系统。
4. 常见问题排查与避坑经验实录
4.1 侧边栏图标点了没反应
这是最常见的问题,排查思路按顺序来:
先确认抽选程序本身能不能独立打开。从应用列表里直接点开,如果能打开,说明程序没问题,问题出在侧边栏的挂载上。如果独立打开也失败,那就是程序本身的问题,先解决程序。
侧边栏挂载的问题通常是这几种:快捷方式指向的路径变了(APK 被卸载重装后路径会变)、系统侧边栏缓存了旧的快捷方式信息、或者权限不足。解决办法是删掉侧边栏的快捷方式重新添加,一般能解决。如果还不行,重启机器清一下侧边栏缓存。
4.2 抽选结果和显示不一致
这个问题我遇到过,原因是动画的最终结果和实际抽选结果用了两次随机。正确的做法是:先抽选确定结果,再拿这个结果去做动画,动画过程中显示的是随机名字,但最终定格必须是那个已经确定的结果。如果动画里又抽了一次,就会出现显示和记录不一致的情况。
代码上的关键点是:pickStudent()只调用一次,把返回值传给rollAnimation(),动画结束时显示这个传入的值,而不是再抽一次。
4.3 名单数据丢失
数据丢失通常发生在程序被系统强制杀掉的时候。Android 系统在内存紧张时会杀后台,如果抽选程序在后台被杀,而数据还没写入 localStorage,就会丢。
解决办法有两个:一是每次抽选后立即写入,不要等页面关闭;二是用beforeunload事件做兜底写入。两个都加上,基本不会丢数据了。
window.addEventListener('beforeunload', function() { saveData(); });4.4 不同班级名单切换麻烦
一个老师通常带多个班,每次换班都要重新导入名单太麻烦。我的做法是支持多班级管理:每个班级一份独立数据,用班级名做 key 存在 localStorage 里。界面上加一个班级切换下拉框,切换时加载对应数据。
存储结构改成这样:
{ "currentClass": "高二(3)班", "classes": { "高二(3)班": { "students": [...], "drawn": [...], "weights": {...} }, "高二(5)班": { "students": [...], "drawn": [...], "weights": {...} } } }这个改动让程序的实用性上了一个台阶,老师不用每次换班都折腾名单了。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 侧边栏图标点击无反应 | 快捷方式路径失效 | 删除后重新添加 |
| 抽选结果与显示不符 | 动画中重复随机 | 只抽一次,结果传入动画 |
| 名单数据丢失 | 未及时写入存储 | 每次操作后立即保存 |
| 批量导入名字有乱码 | 编码格式不对 | 统一用 UTF-8 编码 |
| 动画卡顿 | 设备性能不足 | 降低动画帧率或缩短时长 |
| 侧边栏不显示第三方应用 | 系统版本限制 | 改用网页快捷方式方案 |
| 触控不灵敏 | 按钮区域太小 | 触控区域不小于80px |
| 换班后名单没变 | 未切换班级数据 | 使用多班级管理功能 |
4.6 几个我踩过的坑
第一个坑是系统更新导致侧边栏配置重置。六代机有一次系统推送更新后,侧边栏的自定义快捷方式全没了。后来我养成了习惯:每次系统更新后检查侧边栏配置,并且把抽选程序的安装包和配置文件在U盘里备份一份,随时能恢复。
第二个坑是名单里的特殊字符。有学生的名字里有生僻字,在某些字体下显示成方框。解决办法是嵌入一个覆盖生僻字的字体文件,或者用系统默认字体渲染。我选的是后者,因为嵌入字体会增加程序体积。
第三个坑是多台机器同步名单。一开始我想做云端同步,后来发现学校网络环境复杂,云端同步反而增加了不确定性。最后改成用U盘手动同步:名单导出成 JSON 文件,拷到其他机器导入。虽然土,但稳定可靠,不会因为网络问题掉链子。
第四个坑是侧边栏图标被学生误删。有学生好奇长按侧边栏,把抽选图标拖没了。后来我在侧边栏设置里加了锁定功能,锁定后需要密码才能编辑。这个功能系统自带,在侧边栏设置里找“锁定侧边栏”就行。
4.7 性能优化的一点经验
抽选程序本身很轻,但在老机器上还是做了些优化。主要是减少 DOM 操作:滚动动画不重新创建元素,而是复用同一个文本节点,只改textContent。这个改动让动画在老机器上也能跑满 60 帧。
另外,名单数据在内存里缓存一份,不要每次抽选都从 localStorage 读。localStorage 的读写在主线程上是同步的,频繁读写会阻塞 UI。我的做法是启动时读一次到内存,之后所有操作都在内存里做,只在保存时写回 localStorage。
这些优化单独看都不起眼,但叠加起来,在老机器上的体验提升是明显的。课堂工具的第一要求是流畅,卡一下老师的节奏就断了。
最后分享一个实际使用中的小技巧:抽选按钮旁边我加了一个“再抽一次”的快捷按钮,但它的逻辑是“把当前结果放回池子,重新抽”。这样老师如果觉得抽到的人不合适,可以快速重抽,而不用去管理界面重置。这个功能用到的频率比我想象的高,算是意外收获。