Android TV/工位机 智能看板开发踩坑指南:WebView 常见问题与完整解决方案
在开发运行于智能电视或工位机的 Android 看板应用时,我们经常会选择使用 WebView 套壳的方式来加载前端 H5 页面。这种方式开发效率高、迭代快,且能做到多端复用。
然而,在 Android TV 这类特殊的设备上(大屏、无触摸、长时间无人值守、常接外设),往往会遇到一些在普通手机上很难发现的"坑"。本文将详细总结我们在开发电视看板应用时遇到的几个典型问题,并提供从 Android 端到 Web 端的完整解决方案。
1. 遥控器按键无响应(WebView 焦点问题)
🐛 业务场景与表现症状
场景 :业务需要在看板上通过遥控器的方向键来切换轮播图,或者通过确认键来展开某个弹窗。 表现 :在前端 H5 页面中,Web 开发者通过 JS 监听了全局的键盘事件:window.addEventListener('keydown', handleKeyDown)。这段代码在 PC 浏览器或手机上测试一切正常。但是把 App 安装到电视上后,无论怎么按遥控器,页面没有任何响应。
🔍 原因分析
这通常是因为 WebView 没有获取到焦点 (Focus) 。 在普通触摸屏设备上,用户用手指点击 WebView 区域时,系统会自动将焦点赋予它。但在电视这种非触摸设备上,焦点默认可能停留在了外层容器(如 FrameLayout 或 Activity 的根视图)上。 此时,遥控器的按键事件会被 Android 原生的外层 ViewGroup 拦截,根本传不到 WebView 的内核里,导致 JS 的 keydown 事件永远无法触发。特别是在我们动态创建 WebView 实例并 addView 时,如果没有显式配置,它是不可聚焦的。
🛠️ 解决方案
要彻底解决这个问题,需要 Android 端和 Web 端共同配合。
【Android 端配置】
Android 端的核心任务是:允许 WebView 获取焦点,并主动将焦点移交给它。
步骤一:开启 WebView 的焦点能力 在自定义 WebView 的初始化代码中,显式开启焦点相关属性:
kotlin
// WebView.kt
init {
// 允许 WebView 获取焦点
isFocusable = true
isFocusableInTouchMode = true
}
步骤二:页面加载后主动请求焦点 在 Activity 加载 URL 之后,立刻让 WebView 请求焦点,确保后续的按键事件直接发往 WebView:
kotlin
// MainActivity.kt
private fun loadWebView(url: String) {
// ... 添加 WebView 到容器中 ...
webView.loadUrl(url)
// 必须:主动请求焦点,确保能接收遥控器按键
webView.requestFocus()
}
【Web 前端配合】
虽然 Android 端把焦点交给了 WebView 容器,但 Web 页面内部如果写得不规范,依然可能收不到事件。Web 端需要注意以下几点:
-
绑定在正确的对象上 :如果只是监听全局按键,建议绑定在
window或document上,而不是某个特定的div上(除非那个div获得了焦点)。javascript// 推荐做法 window.addEventListener('keydown', function(event) { console.log('按键被按下:', event.key, event.keyCode); // 遥控器方向键通常对应:ArrowUp, ArrowDown, ArrowLeft, ArrowRight // 确认键通常对应:Enter }); -
局部容器监听需加 tabindex :如果业务确实需要监听某个特定
div(比如一个自定义的弹窗组件)的键盘事件,必须给这个 HTML 元素添加tabindex属性,否则普通元素无法获焦。html<!-- 添加 tabindex 使其可获焦 --> <div id="my-dialog" tabindex="0">弹窗内容</div>javascriptconst dialog = document.getElementById('my-dialog'); // 必须先让元素获焦 dialog.focus(); dialog.addEventListener('keydown', handleKey);
2. 插入或拔出 USB 设备导致页面"白屏"重载
🐛 业务场景与表现症状
场景 :工位机看板通常会连接一些外部设备,比如工人用来扫码的 USB 扫码枪,或者调试用的 USB 键盘鼠标。 表现:当机器在正常展示看板画面时,突然拔出或插入一个 USB 设备(如扫码枪),看板画面会突然"白屏"一下,然后重新加载显示。这给用户的体验非常差,就像程序崩溃重启了一样。
🔍 原因分析
这是 Android 经典的 Configuration Change(配置变更) 问题。 当插拔 USB 输入设备时,Android 系统底层的硬件状态发生了改变(触发了 keyboard、keyboardHidden、navigation 等配置变更广播)。 Android 系统应对配置变更的默认行为是极其暴力的:销毁当前的 Activity,并重新创建一个新的 Activity 实例 ,以便应用能加载适应新硬件状态的资源。 Activity 重建意味着里面的 WebView 也会经历销毁和重新实例化的过程,URL 被重新请求,这个重新加载的空窗期就表现为视觉上的"白屏闪烁"。
🛠️ 解决方案
我们需要在 Android 的清单文件中声明接管这些配置变更,告诉系统:"当这些特定配置发生变化时,不要重启我的 Activity,我自己会处理(或者直接忽略它)。"
【Android 端配置】
在 AndroidManifest.xml 中,找到你的看板 <activity> 标签,添加 android:configChanges 属性,把可能触发重启的硬件变更都加上:
xml
<!-- AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard|navigation|uiMode|screenLayout|smallestScreenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
关键属性说明:
keyboard/keyboardHidden:防止 USB 键盘、扫码枪插拔导致重启。navigation:防止遥控器、游戏手柄等导航设备连接状态变化导致重启。uiMode:防止某些设备连接后系统切换 UI 模式导致重启。orientation/screenSize:顺便防止屏幕旋转或分辨率被调整时导致重启(电视上较少发生,但工位机常见)。
3. 内存泄漏与长时间运行崩溃 (OOM)
🐛 业务场景与表现症状
场景 :电视看板的核心要求是 7x24 小时不间断运行 ,且经常会在多个看板页面之间切换轮播。 表现:App 刚启动时一切正常,但运行了几天(或者频繁切换了几十次页面)后,App 突然闪退回到桌面,或者系统弹窗提示"内存不足"。通过 Android Studio 的 Profiler 观察,发现内存占用呈阶梯状持续上升,从未回落。
🔍 原因分析
WebView 是 Android 内存泄漏的"重灾区"。
- 未清理底层资源 :WebView 内部由复杂的 C++ Chromium 内核驱动,如果在 Activity 销毁(或丢弃旧 WebView)时没有正确调用
destroy(),这些底层资源将一直驻留。 - Context 泄漏:WebView 常常会隐式持有传入的 Activity Context。如果不销毁 WebView,Activity 实例就无法被垃圾回收器 (GC) 回收,导致严重的内存泄漏。
- 如果每次用户输入新 URL,我们都
new WebView()但没有清理旧的,那内存很快就会被撑爆。
🛠️ 解决方案
【Android 端配置】
必须在 Activity 的 onDestroy 生命周期,或者在移除旧 WebView 替换新 WebView 时,进行彻底的清理工作。
kotlin
// MainActivity.kt
// 场景1:重新加载新的 URL 时,先清理旧的 WebView
private fun loadWebView(url: String? = null) {
if (::webView.isInitialized) {
webViewContainer.removeView(webView)
webView.destroy() // 必须调用
}
webView = WebView(this)
// ... 配置和加载
}
// 场景2:Activity 意外销毁或退出时,清理当前 WebView
override fun onDestroy() {
if (::webView.isInitialized) {
// 1. 先从父容器中移除
webViewContainer.removeView(webView)
// 2. 销毁内核资源
webView.destroy()
}
super.onDestroy()
unregisterReceiver(networkReceiver)
}
4. 视频/音频自动播放限制
🐛 业务场景与表现症状
场景 :公司年会的看板页面需要播放一段带有背景音乐的欢迎视频;或者产线看板在出现故障时,需要用语音播报"XX工位出现异常"。 表现 :在前端 H5 页面中写了 <video autoplay>,或者在 JS 中直接调用 audio.play()。结果在看板上打开时,视频停留在第一帧不播放,或者音频根本没有声音。但如果外接个鼠标在屏幕上随便点一下,媒体就突然开始播放了。
🔍 原因分析
这是现代浏览器的安全策略。为了防止恶意网页在用户不知情的情况下突然播放大音量的广告吓到用户,Chromium 内核默认严格禁止在没有用户交互(User Gesture)的情况下自动播放带有声音的媒体。 然而,看板是"无人值守"的设备,根本不会有用户去点击屏幕,这就导致媒体永远无法自动播放。
🛠️ 解决方案
对于看板这种可控的内部应用,我们需要在 Android 壳子层面上"赋予特权",绕过这个限制。
【Android 端配置】
在 WebView 的 WebSettings 中,将"媒体播放需要用户手势"这一限制关闭:
kotlin
// WebView.kt
init {
settings = AwSettings(context, true, false, false, false, false).apply {
javaScriptEnabled = true
domStorageEnabled = true
// 核心配置:允许视频/音频自动播放,无需用户交互(特别适合看板场景)
mediaPlaybackRequiresUserGesture = false
}
}
【Web 前端配合】
Android 端放开限制后,Web 端就可以像以前一样自由使用了。 如果是视频,直接使用 autoplay 属性:
html
<!-- 直接自动播放,且可以带声音 -->
<video src="promo.mp4" autoplay loop></video>
如果是音频或语音播报,直接在 JS 中调用:
javascript
const audio = new Audio('alert.mp3');
// Android 端放开限制后,这里的 play() 不会再抛出 NotAllowedError 异常
audio.play().catch(e => console.error('播放失败:', e));
总结
开发 Android TV/工位机的 WebView 壳应用,看似只是写两行代码加载个网页,但由于设备使用场景(无触摸、接外设、长期运行)的特殊性,需要我们对 Android 系统的焦点机制、配置变更以及 WebView 的生命周期有深入的了解。 只有 Android 端打好坚实的基础(处理焦点、拦截配置、管好内存、放开权限),再配合 Web 端的规范编码,才能打造出一个稳定、流畅、省心的智能看板系统。