You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在带翻译与高亮的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();
        }
    }
}

关键功能说明

  1. 状态记录:通过currentSpeechPosition记录当前播放到的文本索引,isPaused标记暂停状态,currentSpeechText保存当前朗读文本
  2. 暂停实现:点击暂停按钮时调用textToSpeech.stop()终止当前播放,但不销毁TTS实例,同时切换UI显示播放按钮
  3. 续播实现:点击播放按钮时,截取currentSpeechText中从currentSpeechPosition开始的剩余文本,调用speakText方法从该位置开始朗读
  4. 高亮同步:在onRangeStart回调中更新currentSpeechPosition,并同步更新文本高亮,确保高亮位置与播放进度一致
  5. 代码优化:修复原代码中德语语言码错误,拆分嵌套的点击事件逻辑,提升代码可读性
  6. 资源管理:在onDestroy中释放TTS资源,避免内存泄漏

内容的提问来源于stack exchange,提问作者Faizan Ul Hassan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 11:55:23