iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
AI Engineering

從行程到應援:30 天做出追星實用工具箱系列 第 20 篇

Day 20:即時打字機效果與字幕閃爍問題解決方案

  • 分享至 

  • xImage
  •  

一、前言

用過即時字幕的人現在會遇到的問題:

  1. 畫面一直閃:每來一句新字幕,整個畫面都跟著抖一下。
  2. 字突然跳出來:一大段翻譯一次全部蹦出來,眼睛來不及跟上,讀起來很累。

為什麼會這樣?

  • 閃爍:App 分不清哪張字幕卡片是舊的、哪張是新的,有新字幕進來時,它乾脆把所有卡片都拆掉重畫一遍,所以畫面會閃。
  • 跳閃:翻譯結果是整段一次送回來的,畫面也就整段一次顯示,中間沒有過渡。

今天要做什麼?

目標 做法 效果
不再閃爍 給每張字幕卡片一個固定的「身分證」,讓 App 認得舊卡片,只畫新增的那張 畫面穩定,不抖動
逐字出現 像真人打字一樣,一個字一個字慢慢出現 視覺順暢,眼睛不吃力
增加整體流暢性 上面兩項一起做好 翻譯出現時不卡、不跳

二、今日任務

Step 1: 實作打字機動畫元件

在 lib/features/live_translate/presentation/widgets/ 下建立 typewriter_text.dart

import 'dart:async';
import 'package:flutter/material.dart';

class TypewriterText extends StatefulWidget {
  final String text;
  final TextStyle? style;
  final Duration speed;

  const TypewriterText({
    Key? key,
    required this.text,
    this.style,
    this.speed = const Duration(milliseconds: 30), // 逐字打字間隔
  }) : super(key: key);

  @override
  State<TypewriterText> createState() => _TypewriterTextState();
}

class _TypewriterTextState extends State<TypewriterText> {
  String _displayedText = '';
  Timer? _timer;
  int _currentIndex = 0;

  @override
  void initState() {
    super.initState();
    _startTypewriter();
  }

  @override
  void didUpdateWidget(covariant TypewriterText oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 當文字有更新時,從當前長度繼續打字,避免重頭開始閃爍
    if (oldWidget.text != widget.text) {
      _startTypewriter();
    }
  }

  void _startTypewriter() {
    _timer?.cancel();

    // 若新文字比已顯示短,重置
    if (widget.text.length < _displayedText.length) {
      _currentIndex = 0;
      _displayedText = '';
    }

    _timer = Timer.periodic(widget.speed, (timer) {
      if (_currentIndex < widget.text.length) {
        setState(() {
          _currentIndex++;
          _displayedText = widget.text.substring(0, _currentIndex);
        });
      } else {
        _timer?.cancel();
      }
    });
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text(
      _displayedText,
      style: widget.style,
    );
  }
}

Step 2: 整合至字幕卡片與 Key 穩定化

更新 live_subtitle_list_view.dart,加入 ValueKey 並套用打字機元件

// 1. 為 SubtitleItem 加上唯一識別 ID
class SubtitleItem {
  final String id;             // 唯一識別碼
  final String rawText;
  final String translatedText;

  SubtitleItem({
    required this.id,
    required this.rawText,
    required this.translatedText,
  });
}

// 2. 在 ListView.builder 中綁定 Key 並套用 TypewriterText
Widget _buildSubtitleCard(SubtitleItem item, int index) {
  return Container(
    key: ValueKey(item.id), // 防閃爍:給予固定的 Key,讓 Flutter 知道這是同一個 Widget
    margin: const EdgeInsets.only(bottom: 12.0),
    padding: const EdgeInsets.all(14.0),
    decoration: BoxDecoration(
      color: Colors.grey.shade900,
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: Colors.white12),
    ),
    child: Column(
      crossAxisAlignment: CrossAlignment.start,
      children: [
        // 原始逐字稿
        Text(
          item.rawText,
          style: TextStyle(fontSize: 13, color: Colors.grey.shade400),
        ),
        const SizedBox(height: 6),
        // 使用打字機元件呈現譯文,消除閃爍
        TypewriterText(
          text: item.translatedText,
          style: const TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.w600,
            color: Colors.amberAccent,
            height: 1.4,
          ),
        ),
      ],
    ),
  );
}

上一篇
Day 19:Flutter 滾動字幕 UI 繪製/自動置底與滾動動畫優化
系列文
從行程到應援:30 天做出追星實用工具箱 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言