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

Android应用中TTS朗读时实现文本视图自动滚动的方案咨询

实现TTS朗读时文本容器自动滚动的方案

嘿Carlos,你的TTS文本高亮功能已经做得很扎实了!针对长文本需要自动滚动到当前朗读位置的需求,我给你整理了几个关键的修改点和完整的实现方案:

现有代码的小问题

先说说当前代码里可以优化的地方:

  • 每次onRangeStart都重新创建SpannableString并设置MovementMethod,重复操作既浪费资源也没必要
  • 缺少滚动到当前高亮区域的核心逻辑

具体修改方案

1. 提前配置TextView的滚动属性

在onCreate方法里就完成TextView的滚动方法设置,不用每次朗读时重复操作:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    textView = findViewById(R.id.text_view_id);
    textView.setText(sentence);
    // 提前设置滚动方法,只需一次
    textView.setMovementMethod(new ScrollingMovementMethod());
    tts = new TextToSpeech(this, this);
    botao = findViewById(R.id.botao);
    botao.setOnClickListener(this);
}

2. 优化Spannable的复用与高亮逻辑

每次朗读到新范围时,复用现有文本的Spannable对象,清除之前的高亮后再添加新的高亮,更高效:

3. 添加自动滚动到当前朗读位置的核心代码

在onRangeStart的UI线程操作里,计算当前高亮区域对应的行位置,然后让TextView滚动到该位置,确保高亮内容始终在屏幕可见范围内:

@Override
public void onRangeStart(String utteranceId, final int start, final int end, int frame) {
    Log.i("XXX", "onRangeStart() ... utteranceId: " + utteranceId + ", start: " + start + ", end: " + end + ", frame: " + frame);
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            // 复用当前TextView的文本作为Spannable,避免重复创建
            Spannable textWithHighlights = (Spannable) textView.getText();
            // 清除之前的所有背景高亮Span
            BackgroundColorSpan[] oldSpans = textWithHighlights.getSpans(0, textWithHighlights.length(), BackgroundColorSpan.class);
            for (BackgroundColorSpan span : oldSpans) {
                textWithHighlights.removeSpan(span);
            }
            // 添加新的高亮Span
            textWithHighlights.setSpan(new BackgroundColorSpan(Color.YELLOW), start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            
            // 核心:滚动到当前朗读的位置
            if (textView.getLayout() != null) {
                // 获取当前起始位置对应的行
                int line = textView.getLayout().getLineForOffset(start);
                // 获取该行的顶部坐标
                int lineTop = textView.getLayout().getLineTop(line);
                // 滚动到该行顶部,预留1/4屏幕高度让内容更居中
                textView.scrollTo(0, lineTop - textView.getHeight() / 4);
            }
        }
    });
}

额外优化:平滑滚动效果

如果想要更自然的滚动体验,可以用ValueAnimator实现平滑滚动,替代直接的scrollTo:

// 替换上述scrollTo代码
ValueAnimator animator = ValueAnimator.ofInt(textView.getScrollY(), lineTop - textView.getHeight() / 4);
animator.setDuration(300);
animator.addUpdateListener(animation -> {
    int scrollY = (int) animation.getAnimatedValue();
    textView.scrollTo(0, scrollY);
});
animator.start();

完整修改后的Java代码

package com.example.myapplication;
import android.animation.ValueAnimator;
import android.graphics.Color;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.text.Spannable;
import android.text.Spanned;
import android.text.method.ScrollingMovementMethod;
import android.text.style.BackgroundColorSpan;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.example.myapplication.R;

public class MainActivity extends AppCompatActivity implements TextToSpeech.OnInitListener, View.OnClickListener {
    TextToSpeech tts;
    String sentence = "It's possible to perform frequency separation just like in Photoshop, and there are three types of healing tools. Photopea supports actions as well, so it's possible to create frequency separation layers with a single click.";
    TextView textView;
    Button botao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.text_view_id);
        textView.setText(sentence);
        // 提前设置滚动方法
        textView.setMovementMethod(new ScrollingMovementMethod());
        tts = new TextToSpeech(this, this);
        botao = findViewById(R.id.botao);
        botao.setOnClickListener(this);
    }

    public void onInit(int status) {
        tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
            @Override
            public void onStart(String utteranceId) {
                Log.i("XXX", "utterance started");
            }

            @Override
            public void onDone(String utteranceId) {
                Log.i("XXX", "utterance done");
                // 朗读结束后清除高亮
                runOnUiThread(() -> {
                    Spannable text = (Spannable) textView.getText();
                    BackgroundColorSpan[] spans = text.getSpans(0, text.length(), BackgroundColorSpan.class);
                    for (BackgroundColorSpan span : spans) {
                        text.removeSpan(span);
                    }
                });
            }

            @Override
            public void onError(String utteranceId) {
                Log.i("XXX", "utterance error");
            }

            @Override
            public void onRangeStart(String utteranceId, final int start, final int end, int frame) {
                Log.i("XXX", "onRangeStart() ... utteranceId: " + utteranceId + ", start: " + start + ", end: " + end + ", frame: " + frame);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Spannable textWithHighlights = (Spannable) textView.getText();
                        // 清除旧高亮
                        BackgroundColorSpan[] oldSpans = textWithHighlights.getSpans(0, textWithHighlights.length(), BackgroundColorSpan.class);
                        for (BackgroundColorSpan span : oldSpans) {
                            textWithHighlights.removeSpan(span);
                        }
                        // 添加新高亮
                        textWithHighlights.setSpan(new BackgroundColorSpan(Color.YELLOW), start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);

                        // 平滑滚动到当前朗读位置
                        if (textView.getLayout() != null) {
                            int line = textView.getLayout().getLineForOffset(start);
                            int lineTop = textView.getLayout().getLineTop(line);
                            ValueAnimator animator = ValueAnimator.ofInt(textView.getScrollY(), lineTop - textView.getHeight() / 4);
                            animator.setDuration(300);
                            animator.addUpdateListener(animation -> {
                                int scrollY = (int) animation.getAnimatedValue();
                                textView.scrollTo(0, scrollY);
                            });
                            animator.start();
                        }
                    }
                });
            }
        });
    }

    public void startClicked(View ignored) {
        tts.speak(sentence, TextToSpeech.QUEUE_FLUSH, null, "utterance_1");
    }

    @Override
    public void onClick(View view) {
        tts.speak(sentence, TextToSpeech.QUEUE_FLUSH, null, "utterance_1");
    }
}

这样修改后,你的应用就能在TTS朗读长文本时,自动将当前高亮的内容滚动到屏幕可见区域,体验更接近电影字幕的效果啦!

内容的提问来源于stack exchange,提问作者Carlos Pereira

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 12:53:12