Flutter:如何在WebViewWidget中选中指定文本并调用TTS播放音频
我完全理解你的需求——让用户能在WebView里选中专属样式(下划线/高亮)的文本,然后触发TTS朗读。你目前尝试了用GestureDetector搭配WebView的手势识别,但遇到了触发不顺畅的问题,同时也在考虑用JavaScript来实现,我来分享两种可行的解决方案,帮你搞定这个功能:
方案一:优化Dart侧手势识别+JS获取选中文本
WebView本身会消费大部分手势事件,所以外层的GestureDetector经常会“抢不到”事件。我们可以调整WebView的手势识别配置,结合JS来获取并校验选中的文本:
调整WebView的手势识别器:
优先用LongPressGestureRecognizer来触发文本选择(这和Web端默认的选文本逻辑一致,用户体验更自然),然后在长按结束后通过JS获取选中的文本,并检查它是否符合你的样式要求。修改你的WebView代码如下:
child: WebViewWidget( controller: _controller, gestureRecognizers: { Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer() ..onLongPressEnd = (LongPressEndDetails details) async { // 执行JS获取选中的文本,同时校验是否是下划线/高亮文本 String result = await _controller.runJavascriptReturningResult(''' const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (!selectedText) return ''; // 检查选中文本所在元素是否有下划线样式 const selectedElement = selection.anchorNode.parentElement; const isUnderlined = selectedElement.tagName === 'U' || selectedElement.style.textDecoration.includes('underline') || selectedElement.classList.contains('highlighted-text'); // 替换成你的高亮类名 isUnderlined ? selectedText : ''; '''); // 去除JS返回的引号,拿到纯文本 String selectedText = result.replaceAll('"', ''); if (selectedText.isNotEmpty) { TTSHelper.speak(text: selectedText); } }), }, ),注意事项:
- 这段JS会检查选中文本的父元素是否是
<u>标签、带有下划线样式,或者属于自定义的高亮类,你可以根据实际HTML结构调整判断逻辑。 - 记得确保WebViewController已经初始化完成,避免调用
runJavascriptReturningResult时出现空指针。
- 这段JS会检查选中文本的父元素是否是
方案二:JS绑定元素事件+Flutter通信(更稳定推荐)
如果Dart侧的手势冲突不好解决,直接在HTML里用JS给目标文本绑定事件,再通过JavaScriptChannel把文本传给Flutter触发TTS,这个方案更直接可靠:
在Flutter中创建JavaScriptChannel:
先给WebViewController添加一个通信通道,用来接收JS传来的文本:late final WebViewController _controller; @override void initState() { super.initState(); _controller = WebViewController() ..addJavaScriptChannel( 'TTSChannel', onMessageReceived: (JavaScriptMessage message) { String textToSpeak = message.message; if (textToSpeak.isNotEmpty) { TTSHelper.speak(text: textToSpeak); } }, ) ..loadHtmlString(_prepareHTML(content)); }修改
_prepareHTML函数,添加JS代码:
在你的HTML模板里加入一段JS,自动给所有下划线/高亮文本绑定点击(或长按)事件,点击时把文本传给Flutter:String _prepareHTML(String content) { return """<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> body {background-color: ${getCSSColor(context, CustomColors.mainAppBG)};} h1, h2, h3, h4, h5, h6 {color: ${getCSSColor(context, CustomColors.blueWhiteText)};} p {color: ${getCSSColor(context, CustomColors.text)};} table { width: 100%; table-layout: fixed; word-wrap: break-word; } table, tr, th, td { color: ${getCSSColor(context, CustomColors.text)}; border: 1px solid ${getCSSColor(context, CustomColors.text)}; border-collapse: collapse; } th, td { padding: 6px 12px; font-weight: bold; font-size: 1.25em; } /* 给可点击的文本加视觉提示 */ u, .highlighted-text { cursor: pointer; transition: opacity 0.2s; } u:hover, .highlighted-text:hover { opacity: 0.8; } </style> <script> // 页面加载完成后绑定事件 window.addEventListener('load', () => { // 选择所有目标元素:<u>标签和自定义高亮类的元素 const targetTexts = document.querySelectorAll('u, .highlighted-text'); targetTexts.forEach(element => { // 绑定点击事件 element.addEventListener('click', () => { const text = element.textContent.trim(); if (text) { // 发送文本到Flutter侧 TTSChannel.postMessage(text); } }); // 可选:绑定长按事件,和点击二选一即可 element.addEventListener('contextmenu', (e) => { e.preventDefault(); // 阻止默认右键菜单 const text = element.textContent.trim(); if (text) { TTSChannel.postMessage(text); } }); }); }); </script> </head> <body> $content </body> </html>"""; }方案优势:
- 完全避开Flutter和WebView的手势冲突问题,直接在Web层面处理交互。
- 可以给目标文本添加 hover 效果,让用户更清楚哪些文本是可交互的。
- 支持点击或长按两种触发方式,你可以根据需求选择。
两种方案都能实现你的需求,我个人更推荐第二种JS+通信的方案,因为它更稳定,也更容易扩展(比如后续要加其他交互)。
备注:内容来源于stack exchange,提问作者Cedric

