You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

遵循Stack Overflow方案实现TTS后自动点击按钮,代码为何失效?

解决TTS完成后自动点击按钮并避免重叠的问题

咱们先聊聊你当前代码里的几个核心问题,这也是导致效果不达预期的关键:

  • 首先你写的tts.isSpeak()是个笔误,TextToSpeech的正确方法是isSpeaking(),这个小错误可能直接让你的轮询判断失效。
  • 用1秒间隔轮询isSpeaking()的方式太不精准了——TTS可能在两次轮询之间就结束了,导致你错过触发时机;而且只要Handler没被移除,这个轮询会一直跑,白白浪费资源。
  • threadDone的状态管理有漏洞:比如当onTtsFinished里的延迟点击任务还没执行时,轮询的Runnable可能会再次进入onTtsFinished,导致多个点击任务被触发,反而出现你要避免的重叠问题。

其实TextToSpeech本身就提供了监听发音状态的API,完全不需要用轮询这种笨办法。我给你一个更可靠的实现方案,用官方监听器来精准捕获TTS结束事件,同时用标记位严格控制TTS任务的执行,彻底避免重叠:

修改后的核心代码实现

// 标记位:当前是否有TTS任务在运行
private boolean isTtsRunning = false;
private TextToSpeech tts;
// 存储当前TTS对应的shown_answer状态
private boolean currentShownAnswer;

// 初始化TTS时设置监听器(把这段逻辑放到你初始化TTS的地方)
private void initTextToSpeech(Context context) {
    tts = new TextToSpeech(context, status -> {
        if (status == TextToSpeech.SUCCESS) {
            // 这里可以添加语言设置等初始化操作,比如:
            // tts.setLanguage(Locale.US);
            
            // 设置TTS进度监听器,这是核心逻辑
            tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
                @Override
                public void onStart(String utteranceId) {
                    // TTS开始发音,标记为运行中
                    isTtsRunning = true;
                }

                @Override
                public void onDone(String utteranceId) {
                    // TTS发音完成,回到主线程执行点击逻辑(监听器回调在子线程)
                    new Handler(Looper.getMainLooper()).post(() -> {
                        handleTtsCompletion(currentShownAnswer);
                        // 标记TTS任务结束,允许下一次任务
                        isTtsRunning = false;
                    });
                }

                @Override
                public void onError(String utteranceId) {
                    // 发音出错,也要重置标记位
                    isTtsRunning = false;
                }
            });
        }
    });
}

// 启动TTS的方法,调用这个方法来开始发音
public void startTts(String speechText, boolean shownAnswer) {
    // 只有当没有TTS在运行,且TTS初始化成功时,才启动新任务
    if (!isTtsRunning && tts != null && tts.isLanguageAvailable(Locale.getDefault()) >= TextToSpeech.LANG_AVAILABLE) {
        currentShownAnswer = shownAnswer;
        
        // 必须设置UTTERANCE_ID,这样监听器才能识别对应的TTS任务
        HashMap<String, String> ttsParams = new HashMap<>();
        ttsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "duendecat_tts_task");
        
        // 用QUEUE_FLUSH确保如果之前有未完成的任务(理论上不会,因为有标记位),直接替换
        int speakResult = tts.speak(speechText, TextToSpeech.QUEUE_FLUSH, ttsParams);
        if (speakResult != TextToSpeech.SUCCESS) {
            // 处理发音失败的情况,比如提示用户
            isTtsRunning = false;
        }
    }
}

// TTS完成后的点击逻辑
private void handleTtsCompletion(boolean shownAnswer) {
    if (showAnswer && !shownAnswer) {
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            button.performClick();
        }, showDelay);
    }
    if (nextQuestion && shownAnswer) {
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            button.performClick();
        }, nextDelay);
    }
}

为什么这个方案更可靠?

  1. 精准触发:通过OnUtteranceProgressListeneronDone回调,能在TTS发音完成的瞬间触发后续逻辑,完全不会错过时机,也不需要轮询。
  2. 彻底避免重叠isTtsRunning标记位严格控制同一时间只有一个TTS任务在运行,启动前先检查状态,从根源上避免重叠。
  3. 资源更高效:移除了无限轮询的Handler循环,不会一直占用主线程资源。

最后记得把原来的setTimer方法和相关的轮询逻辑全部删掉,改用上面的startTts方法来启动TTS就可以了。

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

火山引擎 最新活动