如何在带翻译与高亮的TTS应用中实现暂停/恢复播放功能
实现TTS暂停续播与文本高亮同步
问题背景
开发多语言翻译+文本转语音(TTS)应用,已实现翻译文本的同步高亮,但暂停时使用stop()会终止TTS引擎,恢复时只能从头播放,需实现从当前高亮位置续播的功能。
原代码存在的问题
- 暂停按钮仅切换UI,未实现真正的暂停逻辑
- TTS实例每次播放都重新创建,无法保留播放状态
- 未记录当前播放的文本位置,续播无法定位
speakTextIV的点击事件嵌套在getLanguageCode方法中,逻辑混乱- 德语语言码错误(原代码用GA爱尔兰语代替DE德语)
解决方案代码
package com.nextsuntech.texttospeech; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.speech.RecognizerIntent; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.style.BackgroundColorSpan; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; import com.google.android.material.button.MaterialButton; import com.google.android.material.textfield.TextInputEditText; import com.google.firebase.ml.common.modeldownload.FirebaseModelDownloadConditions; import com.google.firebase.ml.naturallanguage.FirebaseNaturalLanguage; import com.google.firebase.ml.naturallanguage.translate.FirebaseTranslateLanguage; import com.google.firebase.ml.naturallanguage.translate.FirebaseTranslator; import com.google.firebase.ml.naturallanguage.translate.FirebaseTranslatorOptions; import java.util.ArrayList; import java.util.Locale; public class MainActivity extends AppCompatActivity { Spinner fromSP; Spinner toSP; TextInputEditText sourceText; ImageView micIV; ImageView pauseBT; ImageView playBT; MaterialButton translationBT; TextView translationTV; ImageView speakTextIV; TextToSpeech textToSpeech; String[] fromLanguages = {"From", "English", "Arabic", "Urdu", "Hindi", "German", "Chinese", "Spanish" , "French", "Bengali", "Russian", "Portuguese"}; String[] toLanguages = {"To", "English", "Arabic", "Urdu", "Hindi", "German", "Chinese", "Spanish" , "French", "Bengali", "Russian", "Portuguese"}; private static final int REQUEST_PERMISSION_CODE = 1; int fromLanguageCode, toLanguageCode = 0; // 新增状态变量,记录播放进度与状态 private int currentSpeechPosition = 0; private boolean isPaused = false; private String currentSpeechText = ""; private String currentLanguage = ""; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); fromSP = findViewById(R.id.idFromSpinner); toSP = findViewById(R.id.idToSpinner); sourceText = findViewById(R.id.idEditSource); micIV = findViewById(R.id.idIVMic); translationBT = findViewById(R.id.idBtnTranslation); translationTV = findViewById(R.id.idTranslatedTV); speakTextIV = findViewById(R.id.iv_speak_text); pauseBT = findViewById(R.id.bt_pause); playBT = findViewById(R.id.bt_play); // 提前初始化TTS实例,避免重复创建 initTTS(); fromSP.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { fromLanguageCode = getLanguageCode(fromLanguages[i]); } @Override public void onNothingSelected(AdapterView<?> adapterView) { } }); ArrayAdapter fromAdapter = new ArrayAdapter(this, R.layout.spinner_item, fromLanguages); fromAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); fromSP.setAdapter(fromAdapter); toSP.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { currentLanguage = toLanguages[i]; toLanguageCode = getLanguageCode(currentLanguage); } @Override public void onNothingSelected(AdapterView<?> adapterView) { } }); ArrayAdapter toAdapter = new ArrayAdapter(this, R.layout.spinner_item, toLanguages); toAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); toSP.setAdapter(toAdapter); micIV.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault()); intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Say something to translate"); try { startActivityForResult(intent, REQUEST_PERMISSION_CODE); } catch (Exception e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "" + e.getMessage(), Toast.LENGTH_SHORT).show(); } } }); translationBT.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { translationTV.setVisibility(View.VISIBLE); translationTV.setText(""); if (sourceText.getText().toString().isEmpty()) { Toast.makeText(MainActivity.this, "Please enter text to translate", Toast.LENGTH_SHORT).show(); } else if (fromLanguageCode == 0) { Toast.makeText(MainActivity.this, "Please select source language", Toast.LENGTH_SHORT).show(); } else if (toLanguageCode == 0) { Toast.makeText(MainActivity.this, "Please select the language to translate", Toast.LENGTH_SHORT).show(); } else { translateText(fromLanguageCode, toLanguageCode, sourceText.getText().toString()); } } }); // 暂停按钮逻辑:终止当前播放,记录状态 pauseBT.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (textToSpeech != null && textToSpeech.isSpeaking()) { textToSpeech.stop(); isPaused = true; pauseBT.setVisibility(View.GONE); playBT.setVisibility(View.VISIBLE); } } }); // 播放/续播按钮逻辑:根据状态选择从头播放或续播 playBT.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (isPaused && !currentSpeechText.isEmpty()) { // 续播:从当前记录的位置开始朗读剩余文本 String remainingText = currentSpeechText.substring(currentSpeechPosition); speakText(remainingText, currentLanguage, currentSpeechPosition); isPaused = false; playBT.setVisibility(View.GONE); pauseBT.setVisibility(View.VISIBLE); } else { // 从头播放 startSpeech(); } } }); // 朗读按钮逻辑:触发从头播放 speakTextIV.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { startSpeech(); } }); } // 初始化TTS实例并设置进度监听 private void initTTS() { textToSpeech = new TextToSpeech(getApplicationContext(), status -> { if (status == TextToSpeech.SUCCESS) { Log.i("TTS", "Initialized successfully"); } else { Toast.makeText(MainActivity.this, "TTS initialization failed", Toast.LENGTH_SHORT).show(); } }); // 监听播放进度,更新当前位置与高亮 textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() { @Override public void onStart(String utteranceId) { Log.i("TTS", "utterance started"); } @Override public void onDone(String utteranceId) { Log.i("TTS", "utterance done"); runOnUiThread(() -> { pauseBT.setVisibility(View.GONE); playBT.setVisibility(View.GONE); currentSpeechPosition = 0; isPaused = false; }); } @Override public void onError(String utteranceId) { Log.i("TTS", "utterance error"); } @Override public void onRangeStart(String utteranceId, int start, int end, int frame) { // 更新当前播放位置 currentSpeechPosition = start; // 在主线程更新文本高亮 runOnUiThread(() -> { String fullText = translationTV.getText().toString(); Spannable textWithHighlights = new SpannableString(fullText); // 清除之前的高亮 textWithHighlights.clearSpans(); // 设置当前段落的高亮 textWithHighlights.setSpan(new BackgroundColorSpan(Color.YELLOW), start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE); translationTV.setText(textWithHighlights); }); } }); } // 触发从头播放的统一入口 private void startSpeech() { currentSpeechText = translationTV.getText().toString(); if (currentSpeechText.isEmpty()) { Toast.makeText(this, "No text to speak", Toast.LENGTH_SHORT).show(); return; } if (currentLanguage.equals("To")) { Toast.makeText(this, "Please select target language", Toast.LENGTH_SHORT).show(); return; } currentSpeechPosition = 0; speakText(currentSpeechText, currentLanguage, 0); pauseBT.setVisibility(View.VISIBLE); playBT.setVisibility(View.GONE); isPaused = false; } // 核心朗读方法,支持指定起始位置 private void speakText(String text, String language, int startOffset) { Locale locale = getLocaleForLanguage(language); if (locale != null) { textToSpeech.setLanguage(locale); // 传递起始偏移参数,确保高亮位置匹配 android.os.Bundle params = new android.os.Bundle(); params.putInt(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "UNIQUE_UTTERANCE_ID"); params.putInt(TextToSpeech.Engine.KEY_PARAM_START, startOffset); textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, params, "UNIQUE_UTTERANCE_ID"); } else { Toast.makeText(this, "Unsupported language for TTS", Toast.LENGTH_SHORT).show(); } } // 根据语言名称获取对应Locale private Locale getLocaleForLanguage(String language) { return switch (language) { case "English" -> Locale.ENGLISH; case "Arabic" -> new Locale("ar", "SA"); case "Urdu" -> new Locale("ur"); case "Hindi" -> new Locale("hi"); case "German" -> Locale.GERMAN; case "Chinese" -> Locale.CHINESE; case "Spanish" -> new Locale("es"); case "French" -> Locale.FRENCH; case "Bengali" -> new Locale("bn"); case "Russian" -> new Locale("ru"); case "Portuguese" -> new Locale("pt"); default -> null; }; } private void translateText(int fromLanguageCode, int toLanguageCode, String source) { translationTV.setText("Downloading model, Please wait"); FirebaseTranslatorOptions options = new FirebaseTranslatorOptions.Builder() .setSourceLanguage(fromLanguageCode) .setTargetLanguage(toLanguageCode) .build(); FirebaseTranslator translator = FirebaseNaturalLanguage.getInstance().getTranslator(options); FirebaseModelDownloadConditions conditions = new FirebaseModelDownloadConditions.Builder().build(); translator.downloadModelIfNeeded(conditions).addOnSuccessListener(unused -> { translationTV.setText("Translation..."); translator.translate(source).addOnSuccessListener(s -> translationTV.setText(s)) .addOnFailureListener(e -> Toast.makeText(MainActivity.this, "Failed to translate, Please try again", Toast.LENGTH_SHORT).show()); }).addOnFailureListener(e -> Toast.makeText(MainActivity.this, "Failed to download model, Check your internet connection.", Toast.LENGTH_SHORT).show()); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_PERMISSION_CODE && resultCode == RESULT_OK && data != null) { ArrayList<String> result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); sourceText.setText(result.get(0)); } } // 获取Firebase翻译对应的语言码,修复德语错误 private int getLanguageCode(String language) { return switch (language) { case "English" -> FirebaseTranslateLanguage.EN; case "Arabic" -> FirebaseTranslateLanguage.AR; case "Urdu" -> FirebaseTranslateLanguage.UR; case "German" -> FirebaseTranslateLanguage.DE; case "Hindi" -> FirebaseTranslateLanguage.HI; case "Chinese" -> FirebaseTranslateLanguage.ZH; case "Spanish" -> FirebaseTranslateLanguage.ES; case "French" -> FirebaseTranslateLanguage.FR; case "Bengali" -> FirebaseTranslateLanguage.BN; case "Russian" -> FirebaseTranslateLanguage.RU; case "Portuguese" -> FirebaseTranslateLanguage.PT; default -> 0; }; } // 释放TTS资源,避免内存泄漏 @Override protected void onDestroy() { super.onDestroy(); if (textToSpeech != null) { textToSpeech.stop(); textToSpeech.shutdown(); } } }
关键功能说明
- 状态记录:通过
currentSpeechPosition记录当前播放到的文本索引,isPaused标记暂停状态,currentSpeechText保存当前朗读文本 - 暂停实现:点击暂停按钮时调用
textToSpeech.stop()终止当前播放,但不销毁TTS实例,同时切换UI显示播放按钮 - 续播实现:点击播放按钮时,截取
currentSpeechText中从currentSpeechPosition开始的剩余文本,调用speakText方法从该位置开始朗读 - 高亮同步:在
onRangeStart回调中更新currentSpeechPosition,并同步更新文本高亮,确保高亮位置与播放进度一致 - 代码优化:修复原代码中德语语言码错误,拆分嵌套的点击事件逻辑,提升代码可读性
- 资源管理:在
onDestroy中释放TTS资源,避免内存泄漏
内容的提问来源于stack exchange,提问作者Faizan Ul Hassan
相关产品推荐
相关产品推荐

