IFRAMEs, .contentWindow, .contentDocument区别

本文讨论了使用内联框架(iframes)与JavaScript进行交互的问题。特别指出,在不同浏览器中使用iframe.contentWindow属性来调用frame内的JavaScript函数时,存在兼容性问题。OmniWeb和Safari不支持此属性,而Mozilla和IE可以正常工作。文章提出了使用DOM标准的.contentDocument属性作为替代方案。
from: http://www.omnigroup.com/mailman/archive/omniweb-l/2003/015196.html

> I'm working on a web site that uses inline frames (iframes), with the
> following code to interact with javascripts in the frame:
>
> panelFrame=document.getElementById('panelFrame');
> panelFrame.contentWindow.somefunction();
>
> Now, OmniWeb and Safari both fail on this while Mozilla and IE both
> work. I did some checking, and it turns out that IFRAME.contentWindow
> is not part of the DOM2 spec. Both OmniWeb and Safari support the
> DOM-standard .contentDocument property, which refers to the document
> object of that frame. Javascript global variables and functions,
> however, are stored on the window object, not the document object,
> hence the .contentWindow property that's supported by Mozilla and IE.
(function() { var textNodes = []; var fullText = ''; // 递归遍历DOM树,收集文本节点 function traverse(node) { if (node.nodeType === Node.TEXT_NODE) { var text = node.textContent; // 先不 trim,查看原始文本 console.log('发现文本节点:', text); console.log('文本长度:', text.length); if (text.length > 0) { // 记录文本内容和在完整文本中的起始索引 textNodes.push({ text: text, start: fullText.length }); fullText += text; console.log('已收集文本,当前总长度:', fullText.length); } } else if (node.nodeType === Node.ELEMENT_NODE) { var tag = node.tagName.toLowerCase(); console.log('遍历元素节点:', tag); // 跳过脚本、样式等非内容节点 if (tag !== 'script' && tag !== 'style' && tag !== 'noscript') { for (var i = 0; i < node.childNodes.length; i++) { traverse(node.childNodes[i]); } } } } // 修改容器选择逻辑 var container = document.getElementById('content') || document.querySelector('.content') || document.body; console.log('使用的容器:', container); // 添加这行来实际遍历容器 traverse(container); // 处理iframe中的文本(同域) var iframes = document.getElementsByTagName('iframe'); for (var i = 0; i < iframes.length; i++) { try { var iframeDoc = iframes[i].contentDocument || iframes[i].contentWindow.document; traverse(iframeDoc.body); console.log('iframe处理完成'); } catch (e) { console.log("跨域iframe,无法访问内容:" + e.message); } } // 在函数末尾添加调试信息 console.log('文本收集完成,总文本长度:', fullText.length); console.log('收集到的文本节点数:', textNodes.length); // 将结果传递给Android - 也应该添加错误处理 try { // 将结果传递给Android if (window.TextNodeInterface) { window.TextNodeInterface.onTextNodesCollected(fullText, JSON.stringify(textNodes)); console.log('文本节点收集完成'); } else { console.error("未找到TextNodeInterface接口"); } } catch(e) { console.error('传递数据到Android出错:', e); } })(); 你看看对应调用的js 解释下都是什么意思?
10-13
private fun WebView.executeAutoFillScript(retryCount: Int = 0) { val maxRetries = 3 android.util.Log.d("WebView", "开始执行自动填写脚本,延迟2秒... (重试次数: $retryCount)") postDelayed({ android.util.Log.d("WebView", "开始执行JavaScript自动填写脚本") val autoFillScript = """ (function() { try { if (window.Android && window.Android.log) { window.Android.log('开始自动填写表单 - 页面标题: ' + document.title + ', URL: ' + window.location.href); } // 首先检查页面是否有iframe var iframes = document.querySelectorAll('iframe'); if (iframes.length > 0 && window.Android && window.Android.log) { window.Android.log('发现 ' + iframes.length + ' 个iframe'); for (var i = 0; i < iframes.length; i++) { try { var iframe = iframes[i]; // 方法1: 尝试直接访问iframe内容 var iframeDoc = null; try { iframeDoc = iframe.contentDocument || iframe.contentWindow.document; } catch (e) { window.Android.log('直接访问iframe[' + i + ']失败: ' + e.message); } // 方法2: 如果直接访问失败,尝试通过srcdoc或重新加载同源内容 if (!iframeDoc) { // 检查iframe是否有src属性 var iframeSrc = iframe.src; if (iframeSrc && iframeSrc !== 'about:blank') { window.Android.log('iframe[' + i + '] src: ' + iframeSrc); // 方法3: 尝试通过XMLHttpRequest获取iframe内容 try { var xhr = new XMLHttpRequest(); xhr.open('GET', iframeSrc, false); // 同步请求 xhr.send(); if (xhr.status === 200) { // 创建一个新的iframe来加载内容 var tempDiv = document.createElement('div'); tempDiv.innerHTML = xhr.responseText; // 从响应中提取表单元素 var tempInputs = tempDiv.querySelectorAll('input, textarea, select'); window.Android.log('通过XHR获取iframe[' + i + ']内容,包含 ' + tempInputs.length + ' 个输入元素'); // 处理提取的表单元素 if (tempInputs.length > 0) { processExtractedIframeContent(tempDiv, iframeSrc, i); } } } catch (xhrError) { window.Android.log('XHR获取iframe[' + i + ']内容失败: ' + xhrError.message); } } else if (iframe.srcdoc) { // 处理srcdoc内容 var tempDiv = document.createElement('div'); tempDiv.innerHTML = iframe.srcdoc; var tempInputs = tempDiv.querySelectorAll('input, textarea, select'); window.Android.log('iframe[' + i + '] srcdoc包含 ' + tempInputs.length + ' 个输入元素'); if (tempInputs.length > 0) { processExtractedIframeContent(tempDiv, 'srcdoc', i); } } } else { // 直接访问成功 var iframeInputs = iframeDoc.querySelectorAll('input, textarea, select'); window.Android.log('iframe[' + i + '] 包含 ' + iframeInputs.length + ' 个输入元素'); if (iframeInputs.length > 0) { // 在iframe内执行自动填写 executeInIframe(iframe, iframeDoc); } } } catch (e) { window.Android.log('处理iframe[' + i + ']时出错: ' + e.message); } } } // 定义要填写的用户名和邮箱 var username = 'test'; var email = 'test@qq.com'; // 分析主页面结构 var allInputs = document.querySelectorAll('input, textarea, select'); var allButtons = document.querySelectorAll('button, input[type="button"], input[type="submit"]'); var allForms = document.querySelectorAll('form'); if (window.Android && window.Android.log) { window.Android.log('页面分析 - 输入框: ' + allInputs.length + ', 按钮: ' + allButtons.length + ', 表单: ' + allForms.length); } // 如果主页面有表单元素,优先处理主页面 if (allInputs.length > 0 || allButtons.length > 0 || allForms.length > 0) { fillAndSubmitForm(username, email); } else { // 如果主页面没有表单元素,检查是否处理了iframe内容 if (iframes.length === 0) { if (window.Android && window.Android.onFormError) { window.Android.onFormError('页面中没有找到任何表单元素'); } } else { window.Android.log('主页面没有表单元素,但已处理 ' + iframes.length + ' 个iframe'); } } } catch (error) { if (window.Android && window.Android.onFormError) { window.Android.onFormError('脚本执行错误: ' + error.message); } if (window.Android && window.Android.log) { window.Android.log('错误: ' + error); } } // 处理提取的iframe内容 function processExtractedIframeContent(tempDiv, source, iframeIndex) { try { var username = 'test'; var email = 'test@qq.com'; // 查找表单元素 var inputs = tempDiv.querySelectorAll('input, textarea, select'); var forms = tempDiv.querySelectorAll('form'); var buttons = tempDiv.querySelectorAll('button, input[type="submit"]'); window.Android.log('从' + source + '提取的表单 - 输入框: ' + inputs.length + ', 表单: ' + forms.length + ', 按钮: ' + buttons.length); // 如果找到表单元素,尝试模拟提交 if (inputs.length > 0) { // 这里可以记录表单结构,但无法直接交互 for (var j = 0; j < inputs.length; j++) { var input = inputs[j]; var info = 'iframe[' + iframeIndex + ']字段[' + j + ']: type=' + input.type + ', name=' + input.name + ', placeholder=' + input.placeholder; window.Android.log(info); } // 由于无法直接交互,只能提供信息给用户 if (window.Android && window.Android.onFormDetected) { window.Android.onFormDetected('检测到iframe中的表单,但无法自动填写跨域内容'); } } } catch (error) { window.Android.log('处理提取的iframe内容时出错: ' + error.message); } } // 在iframe内执行自动填写 function executeInIframe(iframe, iframeDoc) { try { var username = 'test'; var email = 'test@qq.com'; // 创建在iframe内执行的脚本 - 修正版本 var iframeScript = "(function() {" + "try {" + " var inputs = document.querySelectorAll('input, textarea, select');" + " var filled = 0;" + " var textInputs = Array.from(document.querySelectorAll('input[type=\\\"text\\\"], input:not([type]), textarea'));" + " if (textInputs.length >= 1) {" + " textInputs[0].value = '" + username + "';" + " textInputs[0].dispatchEvent(new Event('input', { bubbles: true }));" + " filled++;" + " }" + " if (textInputs.length >= 2) {" + " textInputs[1].value = '" + email + "';" + " textInputs[1].dispatchEvent(new Event('input', { bubbles: true }));" + " filled++;" + " }" + " return { success: true, filled: filled, total: inputs.length };" + "} catch (e) {" + " return { success: false, error: e.message };" + "}" + "})();"; // 尝试在iframe中执行脚本 try { var result = iframe.contentWindow.eval(iframeScript); if (result.success) { if (window.Android && window.Android.log) { window.Android.log('在iframe中成功填写了 ' + result.filled + '/' + result.total + ' 个字段'); } // 尝试提交iframe内的表单 setTimeout(function() { submitIframeForm(iframe); }, 1000); } else { if (window.Android && window.Android.log) { window.Android.log('在iframe中执行脚本失败: ' + result.error); } // 尝试使用postMessage tryPostMessageCommunication(iframe); } } catch (evalError) { if (window.Android && window.Android.log) { window.Android.log('无法在iframe中执行脚本: ' + evalError.message); } // 尝试使用postMessage tryPostMessageCommunication(iframe); } } catch (error) { if (window.Android && window.Android.log) { window.Android.log('执行iframe脚本时出错: ' + error.message); } } } // 提交iframe内的表单 function submitIframeForm(iframe) { try { var iframeScript = "(function() {" + "try {" + " var submitted = false;" + " var buttons = document.querySelectorAll('input[type=\\\"submit\\\"], button[type=\\\"submit\\\"], button');" + " for (var i = 0; i < buttons.length; i++) {" + " var text = (buttons[i].textContent || buttons[i].value || '').toLowerCase();" + " if (text.includes('submit') || text.includes('提交') || buttons.length === 1) {" + " buttons[i].click();" + " return { success: true, message: '点击提交按钮' };" + " }" + " }" + " var forms = document.querySelectorAll('form');" + " if (forms.length > 0) {" + " forms[0].submit();" + " return { success: true, message: '通过form.submit()提交' };" + " }" + " return { success: false, message: '未找到提交方式' };" + "} catch (e) {" + " return { success: false, message: '提交错误: ' + e.message };" + "}" + "})();"; var result = iframe.contentWindow.eval(iframeScript); if (result.success) { if (window.Android && window.Android.log) { window.Android.log('iframe表单提交: ' + result.message); } } else { if (window.Android && window.Android.log) { window.Android.log('iframe表单提交失败: ' + result.message); } } } catch (error) { if (window.Android && window.Android.log) { window.Android.log('提交iframe表单时出错: ' + error.message); } } } // 备选方案:通过postMessage与iframe通信 function tryPostMessageCommunication(iframe) { try { // 发送消息给iframe iframe.contentWindow.postMessage({ action: 'autofill', username: 'test', email: 'test@qq.com' }, '*'); // 监听iframe的响应 window.addEventListener('message', function(event) { if (event.data && event.data.action === 'autofill_result') { if (window.Android && window.Android.log) { window.Android.log('通过postMessage通信结果: ' + event.data.message); } } }); } catch (error) { if (window.Android && window.Android.log) { window.Android.log('postMessage通信失败: ' + error.message); } } } // 原有的表单填写和提交函数保持不变 function fillAndSubmitForm(username, email) { try { var filledFields = 0; // 策略1: 尝试所有可能的输入字段 var allInputs = document.querySelectorAll('input, textarea'); if (window.Android && window.Android.log) { window.Android.log('找到 ' + allInputs.length + ' 个输入字段'); } // 记录所有输入字段的信息 for (var i = 0; i < allInputs.length; i++) { var input = allInputs[i]; var info = '字段[' + i + ']: type=' + input.type + ', id=' + input.id + ', name=' + input.name + ', placeholder=' + input.placeholder + ', class=' + input.className; if (window.Android && window.Android.log) { window.Android.log(info); } } // 尝试填写所有文本输入框 var textInputs = Array.from(document.querySelectorAll('input[type="text"], input:not([type]), textarea')); if (window.Android && window.Android.log) { window.Android.log('找到文本输入框: ' + textInputs.length); } if (textInputs.length >= 1) { textInputs[0].value = username; textInputs[0].dispatchEvent(new Event('input', { bubbles: true })); filledFields++; if (window.Android && window.Android.log) { window.Android.log('已填写用户名到第一个文本字段'); } } if (textInputs.length >= 2) { textInputs[1].value = email; textInputs[1].dispatchEvent(new Event('input', { bubbles: true })); filledFields++; if (window.Android && window.Android.log) { window.Android.log('已填写邮箱到第二个文本字段'); } } // 尝试填写邮箱类型的输入框 var emailInputs = Array.from(document.querySelectorAll('input[type="email"]')); if (emailInputs.length > 0) { emailInputs[0].value = email; emailInputs[0].dispatchEvent(new Event('input', { bubbles: true })); filledFields++; if (window.Android && window.Android.log) { window.Android.log('已填写邮箱字段'); } } // 通知填写状态 if (window.Android && window.Android.onFormFilled) { window.Android.onFormFilled('成功填写了 ' + filledFields + ' 个字段'); } if (window.Android && window.Android.log) { window.Android.log('自动填写完成,填写了 ' + filledFields + ' 个字段'); } // 提交表单 setTimeout(function() { submitForm(); }, 1000); } catch (error) { if (window.Android && window.Android.onFormError) { window.Android.onFormError('表单填写错误: ' + error.message); } } } function submitForm() { try { var submitted = false; // 方法1: 查找并点击所有可能的提交按钮 var allButtons = document.querySelectorAll('button, input[type="button"], input[type="submit"]'); if (window.Android && window.Android.log) { window.Android.log('找到按钮: ' + allButtons.length); } for (var i = 0; i < allButtons.length; i++) { var button = allButtons[i]; var text = (button.textContent || button.value || '').toLowerCase(); if (window.Android && window.Android.log) { window.Android.log('按钮[' + i + ']: text=' + text); } // 尝试点击任何看起来像提交的按钮 if (text.includes('submit') || text.includes('提交') || text.includes('login') || text.includes('登录') || text.includes('send') || text.includes('发送') || text.includes('go') || text.includes('确认') || text.includes('下一步') || text.includes('继续') || allButtons.length === 1) { button.click(); submitted = true; if (window.Android && window.Android.log) { window.Android.log('点击按钮: ' + text); } break; } } // 方法2: 表单提交 if (!submitted) { var forms = document.querySelectorAll('form'); if (forms.length > 0) { forms[0].submit(); submitted = true; if (window.Android && window.Android.log) { window.Android.log('通过form.submit()提交'); } } } // 方法3: 尝试触发表单的submit事件 if (!submitted) { var forms = document.querySelectorAll('form'); if (forms.length > 0) { var submitEvent = new Event('submit', { bubbles: true }); forms[0].dispatchEvent(submitEvent); submitted = true; if (window.Android && window.Android.log) { window.Android.log('触发submit事件'); } } } if (submitted) { if (window.Android && window.Android.onFormSubmitted) { window.Android.onFormSubmitted('表单提交成功'); } } else { if (window.Android && window.Android.onFormError) { window.Android.onFormError('无法找到提交方式'); } } } catch (error) { if (window.Android && window.Android.onFormError) { window.Android.onFormError('提交错误: ' + error.message); } } } })(); """.trimIndent() evaluateJavascript(autoFillScript) { result -> android.util.Log.d("WebView", "JavaScript执行完成") // 如果没有找到表单元素,尝试重试 if (retryCount < maxRetries) { android.util.Log.d("WebView", "准备重试表单填写 (${retryCount + 1}/$maxRetries)") executeAutoFillScript(retryCount + 1) } } }, 2000) // 延迟2秒确保页面完全加载 }修改该函数,解决2025-10-21 10:27:43.762 16886-16972 WebViewJS com.example.my1020 D 直接访问iframe[0]失败: Failed to read a named property 'document' from 'Window': Blocked a frame with origin "https://sophiadesign.buzz" from accessing a cross-origin frame. 2025-10-21 10:27:43.762 16886-16972 WebViewJS com.example.my1020 D iframe[0] src: https://test01.sophiadesign.buzz/test/frame.html这个问题
最新发布
10-22
package com.hey.fgh; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; import android.util.Log; import android.view.View; import android.webkit.JavascriptInterface; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.regex.Pattern; import org.json.JSONArray; import org.json.JSONObject; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private WebView mWebView; private TextToSpeech tts; private boolean isSpeaking = false; private boolean isPaused = false; private int currentSentenceIndex = 0; private int currentCharIndex = 0; private String[] sentences; private String fullOriginalText; private List<NodeInfo> nodeInfoList = new ArrayList<>(); private final Handler handler = new Handler(); private SharedPreferences sharedPreferences; private boolean isTTSInitialized = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sharedPreferences = getSharedPreferences("TTSPreferences", MODE_PRIVATE); loadProgress(); initTextToSpeech(); setupWebView(); loadWebContent("file:///android_asset/index.html"); setupButtons(); } private void setupWebView() { mWebView = findViewById(R.id.mWebView); WebSettings settings = mWebView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setAllowFileAccess(true); settings.setAllowContentAccess(true); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { settings.setAllowUniversalAccessFromFileURLs(true); } mWebView.addJavascriptInterface(new TextNodeInterface(), "TextNodeInterface"); mWebView.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { // 注入高亮工具函数 injectHighlightHelper(); // 延迟采集文本节点 handler.postDelayed(() -> collectTextNodes(), 1000); } }); } private void injectHighlightHelper() { String helperJs = "(function() {" + "if (window.createHighlightAtNodeByText) return;" + // 防止重复注入 "window.createHighlightAtNodeByText = function(targetText, localStart, localEnd) {" + " var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);" + " var node;" + " while (node = walker.nextNode()) {" + " if (node.nodeType !== Node.TEXT_NODE || !node.parentNode || node.parentNode.nodeType === Node.ELEMENT_NODE && ['SCRIPT','STYLE'].includes(node.parentNode.tagName)) continue;" + " if (node.textContent.includes(targetText)) {" + " var text = node.textContent;" + " var before = text.substring(0, localStart);" + " var middle = text.substring(localStart, localEnd);" + " var after = text.substring(localEnd);" + " var parent = node.parentNode;" + " parent.innerHTML = '';" + " parent.appendChild(document.createTextNode(before));" + " var mark = document.createElement('mark');" + " mark.className = 'highlight';" + " mark.style.backgroundColor = 'yellow';" + " mark.style.color = 'black';" + " mark.textContent = middle;" + " parent.appendChild(mark);" + " parent.appendChild(document.createTextNode(after));" + " return;" + " }" + " }" + "};" + "})();"; mWebView.evaluateJavascript(helperJs, null); } private void collectTextNodes() { loadJsFromAssets("collect_text_nodes.js"); // 确保这个JS存在assets中 } private void loadJsFromAssets(String fileName) { try { InputStream is = getAssets().open(fileName); byte[] buffer = new byte[is.available()]; is.read(buffer); is.close(); String jsCode = new String(buffer, StandardCharsets.UTF_8); mWebView.evaluateJavascript(jsCode, null); } catch (Exception e) { Log.e(TAG, "加载JS失败: " + fileName, e); Toast.makeText(this, "脚本加载失败", Toast.LENGTH_SHORT).show(); } } private void splitSentences(String rawText) { String filteredText = rawText.replaceAll("[\\r\\n]+", "\n").trim(); List<String> sentenceList = new ArrayList<>(); String[] paragraphs = filteredText.split("\\n\\s*\\n|\\r\\n\\s*\\r\\n"); Pattern sentenceEnd = Pattern.compile("[。.!??!]"); for (String para : paragraphs) { para = para.trim(); if (para.isEmpty()) continue; int last = 0; java.util.regex.Matcher matcher = sentenceEnd.matcher(para); while (matcher.find()) { String sentence = para.substring(last, matcher.end()).trim(); if (!sentence.isEmpty()) { sentenceList.add(sentence); } last = matcher.end(); } if (last < para.length()) { String remaining = para.substring(last).trim(); if (!remaining.isEmpty()) { sentenceList.add(remaining); } } } sentences = sentenceList.toArray(new String[0]); Log.d(TAG, "共分割出 " + sentences.length + " 个句子"); } private void highlightInOriginalText(int globalStart, int globalEnd) { Log.d(TAG, "高亮范围: [" + globalStart + ", " + globalEnd + ")"); if (fullOriginalText == null || globalStart >= globalEnd || globalEnd > fullOriginalText.length()) { return; } List<HighlightRange> ranges = findRangesInNodes(globalStart, globalEnd); if (ranges.isEmpty()) { Log.w(TAG, "未找到匹配节点进行高亮"); return; } StringBuilder js = new StringBuilder(); js.append("(function() {"); js.append(" var oldMarks = document.querySelectorAll('mark.highlight');") .append(" oldMarks.forEach(function(m) {") .append(" var p = m.parentNode;") .append(" p.replaceChild(document.createTextNode(m.textContent), m);") .append(" p.normalize();") .append(" });"); for (HighlightRange range : ranges) { String escapedText = escapeJsString(range.node.text.substring(range.startInNode, range.endInNode)); js.append(String.format( "createHighlightAtNodeByText('%s', %d, %d);", escapedText, range.startInNode, range.endInNode )); } js.append(" setTimeout(function(){") .append(" var h = document.querySelector('mark.highlight');") .append(" if(h)h.scrollIntoView({behavior:'smooth',block:'center'});") .append(" }, 50);"); js.append("})();"); mWebView.evaluateJavascript(js.toString(), null); } private List<HighlightRange> findRangesInNodes(int start, int end) { List<HighlightRange> result = new ArrayList<>(); for (NodeInfo node : nodeInfoList) { int ns = node.start; int ne = node.start + node.text.length(); if (end <= ns || start >= ne) continue; int ls = Math.max(0, start - ns); int le = Math.min(node.text.length(), end - ns); if (ls < le) { result.add(new HighlightRange(node, ls, le)); } } return result; } private String escapeJsString(String s) { return s.replace("\\", "\\\\") .replace("'", "\\'") .replace("\"", "\\\"") .replace("\n", "\\n") .replace("\r", "\\r") .replace("\t", "\\t"); } private void speakFromCurrentPosition() { if (sentences == null || currentSentenceIndex >= sentences.length) { stopSpeaking(); return; } String sentence = sentences[currentSentenceIndex].trim(); if (sentence.isEmpty()) { currentSentenceIndex++; handler.postDelayed(this::speakFromCurrentPosition, 50); return; } int pos = fullOriginalText.indexOf(sentence); if (pos == -1) { // 尝试去除空格再找一次 String cleanFull = fullOriginalText.replaceAll("\\s+", ""); String cleanSent = sentence.replaceAll("\\s+", ""); int idx = cleanFull.indexOf(cleanSent); if (idx != -1) { pos = approximateGlobalPosition(idx, cleanSent.length()); } } if (pos == -1) { Log.w(TAG, "无法定位句子: " + sentence); currentSentenceIndex++; handler.postDelayed(this::speakFromCurrentPosition, 50); return; } int highlightStart = pos + currentCharIndex; int highlightEnd = pos + sentence.length(); highlightInOriginalText(highlightStart, highlightEnd); String toSpeak = sentence.substring(currentCharIndex); Bundle params = new Bundle(); String utteranceId = UUID.randomUUID().toString(); params.putCharSequence(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId); int status = tts.speak(toSpeak, TextToSpeech.QUEUE_FLUSH, params, utteranceId); if (status == TextToSpeech.ERROR) { Log.e(TAG, "TTS 播放错误"); } } private int approximateGlobalPosition(int cleanIndex, int cleanLength) { int realPos = 0; int cleanPos = 0; for (int i = 0; i < fullOriginalText.length(); i++) { char c = fullOriginalText.charAt(i); if (!Character.isWhitespace(c)) { if (cleanPos == cleanIndex) { return i; } cleanPos++; } } return -1; } private void setupButtons() { Button speakButton = findViewById(R.id.speakButton); speakButton.setOnClickListener(v -> { if (isPaused && currentSentenceIndex < sentences.length) { isPaused = false; isSpeaking = true; speakFromCurrentPosition(); } else { startSpeaking(); } }); Button pauseButton = findViewById(R.id.pauseButton); pauseButton.setOnClickListener(v -> { if (isSpeaking) { tts.stop(); isSpeaking = false; isPaused = true; } }); Button stopButton = findViewById(R.id.stopButton); stopButton.setOnClickListener(v -> stopSpeaking()); } private void startSpeaking() { if (sentences == null || sentences.length == 0) { Toast.makeText(this, "尚未加载文本内容", Toast.LENGTH_SHORT).show(); return; } isSpeaking = true; isPaused = false; currentCharIndex = 0; speakFromCurrentPosition(); } private void stopSpeaking() { if (tts != null) { tts.stop(); } isSpeaking = false; isPaused = false; currentCharIndex = 0; saveProgress(); removeHighlight(); } private void removeHighlight() { mWebView.evaluateJavascript("(function(){" + "var ms=document.querySelectorAll('mark.highlight');" + "ms.forEach(m=>{m.parentNode.replaceChild(document.createTextNode(m.textContent),m);});" + "})()", null); } private void saveProgress() { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putInt("currentSentenceIndex", currentSentenceIndex); editor.putInt("currentCharIndex", currentCharIndex); editor.apply(); } private void loadProgress() { currentSentenceIndex = sharedPreferences.getInt("currentSentenceIndex", 0); currentCharIndex = sharedPreferences.getInt("currentCharIndex", 0); } private void initTextToSpeech() { tts = new TextToSpeech(this, status -> { if (status == TextToSpeech.SUCCESS) { isTTSInitialized = true; setUtteranceProgressListener(); } else { Toast.makeText(this, "TTS初始化失败", Toast.LENGTH_SHORT).show(); } }); } private void setUtteranceProgressListener() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { tts.setOnUtteranceProgressListener(new UtteranceProgressListener() { @Override public void onStart(String utteranceId) {} @Override public void onDone(String utteranceId) { handler.post(() -> { currentSentenceIndex++; currentCharIndex = 0; if (currentSentenceIndex < sentences.length) { speakFromCurrentPosition(); } else { isSpeaking = false; saveProgress(); Toast.makeText(MainActivity.this, "朗读完成", Toast.LENGTH_SHORT).show(); } }); } @Override public void onError(String utteranceId) {} }); } } @Override protected void onDestroy() { if (tts != null) { tts.stop(); tts.shutdown(); } super.onDestroy(); } // --- 数据类 --- public static class NodeInfo { String text; int start; NodeInfo(String text, int start) { this.text = text; this.start = start; } } public static class HighlightRange { NodeInfo node; int startInNode, endInNode; HighlightRange(NodeInfo node, int start, int end) { this.node = node; this.startInNode = start; this.endInNode = end; } } // --- JS 接口 --- public class TextNodeInterface { @JavascriptInterface public void onTextNodesCollected(String fullText, String nodeInfoJson) { runOnUiThread(() -> { if (fullText == null || fullText.trim().isEmpty()) { Toast.makeText(MainActivity.this, "未提取到有效文本", Toast.LENGTH_SHORT).show(); return; } fullOriginalText = fullText; nodeInfoList.clear(); try { JSONArray arr = new JSONArray(nodeInfoJson); for (int i = 0; i < arr.length(); i++) { JSONObject obj = arr.getJSONObject(i); String text = obj.getString("text"); int start = obj.getInt("start"); nodeInfoList.add(new NodeInfo(text, start)); } Log.d(TAG, "✅ 成功解析 " + nodeInfoList.size() + " 个文本节点"); } catch (Exception e) { Log.e(TAG, "解析 nodeInfo 失败", e); Toast.makeText(MainActivity.this, "结构解析失败", Toast.LENGTH_SHORT).show(); return; } splitSentences(fullOriginalText); }); } } private void loadWebContent(String url) { mWebView.loadUrl(url); } @Override public void onBackPressed() { if (mWebView.canGoBack()) { mWebView.goBack(); } else { super.onBackPressed(); } }} 这是你修改的java代码 还有个名为:collect_text_nodes.js的代码如何 :// collect_text_nodes.js (function() { var textNodes = []; var fullText = ''; function traverse(node) { if (node.nodeType === Node.TEXT_NODE) { var text = node.textContent; if (text && text.length > 0) { textNodes.push({ text: text, start: fullText.length }); fullText += text; } } else if (node.nodeType === Node.ELEMENT_NODE) { var tag = node.tagName.toLowerCase(); if (tag !== 'script' && tag !== 'style' && tag !== 'noscript') { for (var i = 0; i < node.childNodes.length; i++) { traverse(node.childNodes[i]); } } } } var container = document.getElementById('content') || document.querySelector('.content') || document.body; traverse(container); var iframes = document.getElementsByTagName('iframe'); for (var i = 0; i < iframes.length; i++) { try { var iframeDoc = iframes[i].contentDocument || iframes[i].contentWindow.document; if (iframeDoc) { traverse(iframeDoc.body); } } catch (e) { console.log("跨域iframe不可访问: " + e.message); } } try { if (typeof window.TextNodeInterface !== 'undefined') { window.TextNodeInterface.onTextNodesCollected(fullText, JSON.stringify(textNodes)); } else { console.error("❌ 未注册 TextNodeInterface,请检查 addJavascriptInterface"); } } catch (e) { console.error("❌ 回传数据失败: ", e); } console.log("✅ collect_text_nodes.js 已执行,找到文本节点数:", textNodes.length); })();
10-14
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值