package com.wsnd.wsny;
import android.content.DialogInterface;
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.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import android.app.Activity;
import android.content.res.Configuration;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
public class MainActivity extends Activity implements TextToSpeech.OnInitListener {
private static final String TAG = "HighlightReader";
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 Handler handler = new Handler();
private SharedPreferences sharedPreferences;
private boolean isTTSInitialized = false; // 添加这行:TTS初始化状态标志
// JS交互接口:接收网页文本节点信息
public class TextNodeInterface {
@JavascriptInterface
public void onTextNodesCollected(String fullText, String nodeInfo) {
Log.d(TAG, "收到文本节点信息:fullText长度=" + (fullText == null ? 0 : fullText.length()));
runOnUiThread(() -> {
if (fullText == null || fullText.trim().isEmpty()) {
Toast.makeText(MainActivity.this, "未提取到有效文本", Toast.LENGTH_SHORT).show();
return;
}
fullOriginalText = fullText;
splitSentences(fullText); // 分割句子
});
}
}
// 检查TTS是否可用
private boolean isTTSReady() {
return tts != null && isTTSInitialized && tts.getEngines().size() > 0;
}
@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();
}
// 初始化TTS
private void initTextToSpeech() {
tts = new TextToSpeech(this, this);
setUtteranceProgressListener();
}
// 配置WebView
private void setupWebView() {
mWebView = findViewById(R.id.mWebView);
WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
// 允许JS调用Android接口(跨域配置)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
settings.setAllowUniversalAccessFromFileURLs(true);
}
// 添加JS交互接口
mWebView.addJavascriptInterface(new TextNodeInterface(), "TextNodeInterface");
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.d(TAG, "网页加载完成,开始收集文本节点(延迟2秒适配动态内容)");
// 延迟2秒执行,确保Vue等动态内容渲染完成
handler.postDelayed(() -> collectTextNodes(), 2000);
}
});
}
// 从assets加载JS文件(避免Java字符串转义错误)
private String loadJsFromAssets(String filename) {
try {
InputStream is = getAssets().open(filename);
byte[] buffer = new byte[is.available()];
is.read(buffer);
is.close();
return new String(buffer, "UTF-8");
} catch (IOException e) {
Log.e(TAG, "加载JS文件失败:" + e.getMessage());
return "";
}
}
// 注入JS收集文本节点
private void collectTextNodes() {
String js = loadJsFromAssets("collect_text_nodes.js");
if (js.isEmpty()) {
Log.e(TAG, "JS文件内容为空,无法收集文本");
return;
}
mWebView.evaluateJavascript(js, new ValueCallback<String>() {
@Override
public void onReceiveValue(String result) {
Log.d(TAG, "文本收集JS执行结果:" + result);
}
});
}
// 分割句子(适配过滤后为空的情况)
private void splitSentences(String fullText) {
Log.d(TAG, "原始文本内容:" + fullText);
// Step 1: 过滤特定标签和引用(保持原有逻辑)
String normalRegex = "";
String tempText = fullText.replaceAll(normalRegex, "");
String specialRegex = "";
tempText = tempText.replaceAll(specialRegex, "");
String filteredText = tempText.replaceAll(specialRegex, "");
if (filteredText.trim().isEmpty()) {
filteredText = fullText.trim();
}
// Step 2: 使用增强型正则进行句子分割
List<String> sentenceList = new ArrayList<>();
// 先按段落拆分(利用换行、双换行作为自然分隔)
String[] paragraphs = filteredText.split("\\n\\s*\\n|\\r\\n\\s*\\r\\n"); // 双换行视为段落分割
for (String para : paragraphs) {
para = para.trim();
if (para.isEmpty()) continue;
// 在每个段落内进一步切分为句子
List<String> sentencesInPara = splitParagraphIntoSentences(para);
sentenceList.addAll(sentencesInPara);
}
// 转为数组并赋值
sentences = sentenceList.toArray(new String[0]);
Log.d(TAG, "分割完成,句子数量:" + sentences.length);
if (sentences.length == 0) {
Toast.makeText(this, "未找到可朗读的句子", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "已提取" + sentences.length + "个句子", Toast.LENGTH_SHORT).show();
}
}
// 按句子分隔符切分段落为句子列表
private List<String> splitParagraphIntoSentences(String paragraph) {
List<String> sentenceList = new ArrayList<>();
// 使用正则表达式按中文句号、英文句号、问号、感叹号分割句子
String[] parts = paragraph.split("[。.!??!]");
for (String part : parts) {
if (!part.trim().isEmpty()) {
sentenceList.add(part.trim());
}
}
return sentenceList;
}
// 配置按钮事件
private void setupButtons() {
Button speakButton = findViewById(R.id.speakButton);
speakButton.setOnClickListener(v -> {
if (sentences == null || sentences.length == 0) {
Toast.makeText(this, "无可朗读的内容", Toast.LENGTH_SHORT).show();
return;
}
if (isPaused) {
isPaused = false;
speakFromCurrentPosition();
Toast.makeText(this, "朗读继续", Toast.LENGTH_SHORT).show();
} else {
startSpeaking();
}
});
Button stopButton = findViewById(R.id.stopButton);
stopButton.setOnClickListener(v -> {
stopSpeaking();
Toast.makeText(this, "朗读已停止", Toast.LENGTH_SHORT).show();
});
Button pauseButton = findViewById(R.id.pauseButton);
pauseButton.setOnClickListener(v -> {
if (isSpeaking) {
tts.stop();
isSpeaking = false;
isPaused = true;
Toast.makeText(this, "朗读已暂停", Toast.LENGTH_SHORT).show();
}
});
}
// 开始朗读
private void startSpeaking() {
if (fullOriginalText == null || fullOriginalText.isEmpty()) {
Toast.makeText(this, "未加载到文本", Toast.LENGTH_SHORT).show();
return;
}
if (!isTTSReady()) {
Toast.makeText(this, "TTS服务不可用", Toast.LENGTH_SHORT).show();
return;
}
speakFromCurrentPosition();
}
// 停止朗读
private void stopSpeaking() {
tts.stop();
saveProgress();
clearHighlight();
currentSentenceIndex = 0;
currentCharIndex = 0;
isSpeaking = false;
isPaused = false;
}
// 设置TTS进度监听
private void setUtteranceProgressListener() {
tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
isSpeaking = true;
isPaused = false;
}
@Override
public void onDone(String utteranceId) {
currentSentenceIndex++;
currentCharIndex = 0;
if (currentSentenceIndex < sentences.length) {
handler.postDelayed(MainActivity.this::speakFromCurrentPosition, 100);
} else {
isSpeaking = false;
runOnUiThread(() -> {
clearHighlight();
Toast.makeText(MainActivity.this, "播放完成", Toast.LENGTH_SHORT).show();
});
}
}
@Override
public void onError(String utteranceId) {
isSpeaking = false;
runOnUiThread(() -> Toast.makeText(MainActivity.this, "朗读出错", Toast.LENGTH_SHORT).show());
}
});
}
// 从当前位置开始朗读
private void speakFromCurrentPosition() {
if (sentences == null || currentSentenceIndex >= sentences.length) {
Log.w(TAG, "朗读结束或句子数组为空");
return;
}
String sentenceToSpeak = sentences[currentSentenceIndex].trim();
if (sentenceToSpeak.isEmpty()) {
Log.d(TAG, "跳过空句子,索引: " + currentSentenceIndex);
currentSentenceIndex++;
speakFromCurrentPosition();
return;
}
// 查找句子在原始文本中的位置
int startInOriginal = findSentenceInOriginalText(sentenceToSpeak);
if (startInOriginal == -1) {
Log.e(TAG, "❌ 无法定位句子在原文中的位置: \"" + sentenceToSpeak + "\"");
currentSentenceIndex++;
speakFromCurrentPosition();
return;
}
// 计算高亮范围
int highlightStart = startInOriginal + currentCharIndex;
int highlightEnd = startInOriginal + sentenceToSpeak.length();
if (highlightEnd > fullOriginalText.length()) {
highlightEnd = fullOriginalText.length();
}
Log.d(TAG, "计算高亮范围: start=" + highlightStart + ", end=" + highlightEnd);
// 高亮并朗读
highlightInOriginalText(highlightStart, highlightEnd);
String partToSpeak = sentenceToSpeak.substring(currentCharIndex);
try {
Bundle params = new Bundle();
String utteranceId = UUID.randomUUID().toString();
params.putCharSequence(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
// 添加TTS状态检查
if (tts != null) {
int result = tts.speak(partToSpeak, TextToSpeech.QUEUE_FLUSH, params, utteranceId);
if (result == TextToSpeech.ERROR) {
Log.e(TAG, "TTS朗读失败");
Toast.makeText(this, "朗读失败", Toast.LENGTH_SHORT).show();
}
} else {
Log.e(TAG, "TTS对象为空");
Toast.makeText(this, "TTS未初始化", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "TTS朗读异常: " + e.getMessage());
Toast.makeText(this, "朗读出错: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
currentCharIndex += partToSpeak.length();
}
// 在原始文本中查找句子位置(简化匹配逻辑)
private int findSentenceInOriginalText(String sentence) {
// 移除空格后匹配(提高兼容性)
String originalNoSpace = fullOriginalText.replaceAll("\\s+", "");
String sentenceNoSpace = sentence.replaceAll("\\s+", "");
int index = originalNoSpace.indexOf(sentenceNoSpace);
Log.d(TAG, "查找句子位置: " + sentence + ", 位置: " + index);
return index;
}
// 高亮原始文本中指定范围
private void highlightInOriginalText(int start, int end) {
Log.d(TAG, "准备高亮文本,起始位置: " + start + ", 结束位置: " + end);
if (fullOriginalText == null || start < 0 || end > fullOriginalText.length() || start >= end) {
Log.e(TAG, "高亮失败:无效的范围,start=" + start + ", end=" + end + ", 文本长度=" + (fullOriginalText != null ? fullOriginalText.length() : "null"));
return;
}
String highlightedText = fullOriginalText.substring(start, end);
Log.d(TAG, "即将高亮的内容片段: \"" + highlightedText + "\"");
String highlightJs = "(function(start, end) {"
+ " var style = document.createElement('style');"
+ " style.innerHTML = 'mark.highlight { background-color: yellow !important; }';"
+ " document.head.appendChild(style);"
+ " var oldMarks = document.querySelectorAll('mark.highlight');"
+ " oldMarks.forEach(function(mark) {"
+ " var parent = mark.parentNode;"
+ " parent.replaceChild(document.createTextNode(mark.textContent), mark);"
+ " parent.normalize();"
+ " });"
+ " var currentPos = 0;"
+ " var textNodes = [];"
+ " function traverse(node) {"
+ " if (node.nodeType === Node.TEXT_NODE) {"
+ " var text = node.textContent;"
+ " var nodeStart = currentPos;"
+ " var nodeEnd = currentPos + text.length;"
+ " textNodes.push({node: node, start: nodeStart, end: nodeEnd});"
+ " currentPos = nodeEnd;"
+ " } else if (node.nodeType === Node.ELEMENT_NODE && ['SCRIPT', 'STYLE', 'NOSCRIPT'].indexOf(node.tagName) === -1) {"
+ " for (var i = 0; i < node.childNodes.length; i++) {"
+ " traverse(node.childNodes[i]);"
+ " }"
+ " }"
+ " }"
+ " var container = document.getElementById('main') || document.body;"
+ " traverse(container);"
+ " console.log('JS端收集到的文本节点总数:', textNodes.length);"
+ " console.log('目标高亮区间: [' + start + ', ' + end + ')');"
+ " var matchFound = false;"
+ " textNodes.forEach(function(item) {"
+ " if (item.end > start && item.start < end) {"
+ " matchFound = true;"
+ " var node = item.node;"
+ " var text = node.textContent;"
+ " var hStart = Math.max(0, start - item.start);"
+ " var hEnd = Math.min(text.length, end - item.start);"
+ " var before = document.createTextNode(text.substring(0, hStart));"
+ " var mark = document.createElement('mark');"
+ " mark.className = 'highlight';"
+ " mark.style.backgroundColor = 'yellow';"
+ " mark.textContent = text.substring(hStart, hEnd);"
+ " console.log('【JS日志】创建mark元素,内容: \"' + mark.textContent + '\", 范围: [' + hStart + ', ' + hEnd + ']');"
+ " var parent = node.parentNode;"
+ " parent.insertBefore(before, node);"
+ " parent.insertBefore(mark, node);"
+ " parent.insertBefore(after = document.createTextNode(text.substring(hEnd)), node);"
+ " parent.removeChild(node);"
+ " }"
+ " });"
+ " if (matchFound) {"
+ " console.log('【JS日志】✅ 成功完成高亮');"
+ " } else {"
+ " console.warn('【JS日志】❌ 未找到匹配的文本节点进行高亮');"
+ " }"
+ " setTimeout(function() {"
+ " var mark = document.querySelector('mark.highlight');"
+ " if (mark) {"
+ " mark.scrollIntoView({ behavior: 'smooth', block: 'center' });"
+ " console.log('【JS日志】✅ 滚动到高亮位置');"
+ " } else {"
+ " console.warn('【JS日志】⚠️ 页面上没有发现 mark.highlight 元素');"
+ " }"
+ " }, 100);"
+ "})(" + start + ", " + end + ");";
// 注入JS后监听返回值
mWebView.evaluateJavascript(highlightJs, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Log.d(TAG, "高亮JS执行结果回调: " + value);
if (value == null || value.equals("null")) {
Log.d(TAG, "✅ 高亮JS已执行(无返回值是正常的)");
} else {
Log.d(TAG, "JS返回值: " + value);
}
}
});
// Java层确认调用发出
Log.d(TAG, "✅ 已发送高亮指令,范围 [" + start + ", " + end + "),内容: \"" + highlightedText + "\"");
}
// 清除所有高亮
private void clearHighlight() {
String clearJs = "var marks = document.querySelectorAll('mark.highlight');" +
"marks.forEach(function(mark) {" +
" var parent = mark.parentNode;" +
" parent.replaceChild(document.createTextNode(mark.textContent), mark);" +
" parent.normalize();" +
"});";
mWebView.evaluateJavascript(clearJs, null);
}
// 加载网页内容
private void loadWebContent(String url) {
try {
mWebView.loadUrl(url);
} catch (Exception e) {
Toast.makeText(this, "加载失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "加载失败", e);
}
}
// 加载上次进度
private void loadProgress() {
currentSentenceIndex = sharedPreferences.getInt("currentSentenceIndex", 0);
currentCharIndex = sharedPreferences.getInt("currentCharIndex", 0);
Log.d(TAG, "加载进度:句子=" + currentSentenceIndex + ", 字符=" + currentCharIndex);
}
// 保存当前进度
private void saveProgress() {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putInt("currentSentenceIndex", currentSentenceIndex);
editor.putInt("currentCharIndex", currentCharIndex);
editor.apply();
Log.d(TAG, "保存进度:句子=" + currentSentenceIndex + ", 字符=" + currentCharIndex);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
public void onBackPressed() {
if (mWebView.canGoBack()) {
mWebView.goBack();
} else {
super.onBackPressed();
}
}
@Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
// 设置语言并检查支持情况
int result = tts.setLanguage(Locale.CHINESE);
if (result == TextToSpeech.LANG_MISSING_DATA) {
Toast.makeText(this, "缺少TTS数据", Toast.LENGTH_SHORT).show();
Log.e(TAG, "TTS缺少语言数据");
isTTSInitialized = false;
} else if (result == TextToSpeech.LANG_NOT_SUPPORTED) {
Toast.makeText(this, "不支持中文朗读", Toast.LENGTH_SHORT).show();
Log.e(TAG, "TTS不支持中文语言");
isTTSInitialized = false;
} else {
Log.d(TAG, "TTS初始化成功(支持中文)");
isTTSInitialized = true; // 设置初始化成功标志
}
} else {
Toast.makeText(this, "TTS初始化失败", Toast.LENGTH_SHORT).show();
Log.e(TAG, "TTS初始化失败,状态码=" + status);
isTTSInitialized = false;
}
}
@Override
protected void onDestroy() {
if (tts != null) {
tts.stop();
tts.shutdown();
}
super.onDestroy();
}
} 你看下这段代码,认真解释下。
最新发布