用過即時字幕的人現在會遇到的問題:
為什麼會這樣?
今天要做什麼?
| 目標 | 做法 | 效果 |
|---|---|---|
| 不再閃爍 | 給每張字幕卡片一個固定的「身分證」,讓 App 認得舊卡片,只畫新增的那張 | 畫面穩定,不抖動 |
| 逐字出現 | 像真人打字一樣,一個字一個字慢慢出現 | 視覺順暢,眼睛不吃力 |
| 增加整體流暢性 | 上面兩項一起做好 | 翻譯出現時不卡、不跳 |
在 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,
);
}
}
更新 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,
),
),
],
),
);
}