Flutter简易字幕制作器:打造专业级字幕编辑工具
项目概述
简易字幕制作器是一款基于Flutter开发的专业字幕编辑应用,为视频创作者和字幕制作人员提供完整的字幕制作解决方案。应用集成了字幕编辑、时间轴管理、实时预览、多格式导出等核心功能,通过直观的界面设计和强大的编辑能力,让字幕制作变得简单高效。
运行效果图





应用特色
- 可视化编辑:直观的字幕编辑界面,支持实时预览
- 时间轴管理:专业的时间轴视图,精确控制字幕时间
- 多格式导出:支持SRT、VTT、TXT等主流字幕格式
- 项目管理:完整的项目管理系统,支持多项目切换
- 播放控制:内置播放器,实时预览字幕效果
- 智能排列:自动调整字幕时间,避免重叠冲突
技术架构
核心技术栈
yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
项目结构
lib/
├── main.dart # 应用入口和主要逻辑
├── models/ # 数据模型(集成在main.dart中)
│ ├── subtitle_item.dart # 字幕条目模型
│ ├── subtitle_project.dart # 字幕项目模型
│ └── player_state.dart # 播放器状态枚举
├── screens/ # 页面组件(集成在main.dart中)
│ ├── editor_page.dart # 编辑页面
│ ├── timeline_page.dart # 时间轴页面
│ ├── preview_page.dart # 预览页面
│ └── projects_page.dart # 项目页面
├── widgets/ # 自定义组件(集成在main.dart中)
│ ├── player_controls.dart # 播放控制器
│ ├── subtitle_editor.dart # 字幕编辑器
│ ├── timeline_view.dart # 时间轴视图
│ └── subtitle_list.dart # 字幕列表
└── utils/ # 工具类(集成在main.dart中)
├── time_formatter.dart # 时间格式化
├── subtitle_exporter.dart # 字幕导出
└── timeline_painter.dart # 时间轴绘制
数据模型设计
字幕条目模型(SubtitleItem)
字幕条目是应用的核心数据结构,包含字幕的所有属性:
dart
class SubtitleItem {
final String id; // 唯一标识
String text; // 字幕文本
Duration startTime; // 开始时间
Duration endTime; // 结束时间
TextStyle style; // 文本样式
Alignment alignment; // 对齐方式
Color backgroundColor; // 背景颜色
double opacity; // 透明度
SubtitleItem({
required this.id,
required this.text,
required this.startTime,
required this.endTime,
this.style = const TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.normal,
),
this.alignment = Alignment.bottomCenter,
this.backgroundColor = Colors.black54,
this.opacity = 0.8,
});
}
字幕条目模型包含实用的计算属性和方法:
dart
// 计算字幕持续时间
Duration get duration => endTime - startTime;
// 判断字幕在指定时间是否激活
bool isActiveAt(Duration time) {
return time >= startTime && time <= endTime;
}
// 创建字幕副本(用于编辑)
SubtitleItem copyWith({
String? text,
Duration? startTime,
Duration? endTime,
TextStyle? style,
Alignment? alignment,
Color? backgroundColor,
double? opacity,
}) {
return SubtitleItem(
id: id,
text: text ?? this.text,
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
style: style ?? this.style,
alignment: alignment ?? this.alignment,
backgroundColor: backgroundColor ?? this.backgroundColor,
opacity: opacity ?? this.opacity,
);
}
字幕项目模型(SubtitleProject)
字幕项目模型管理整个字幕制作项目:
dart
class SubtitleProject {
final String id; // 项目唯一标识
String name; // 项目名称
Duration totalDuration; // 项目总时长
List<SubtitleItem> subtitles; // 字幕列表
DateTime createdAt; // 创建时间
DateTime updatedAt; // 更新时间
SubtitleProject({
required this.id,
required this.name,
required this.totalDuration,
required this.subtitles,
required this.createdAt,
required this.updatedAt,
});
}
项目模型提供完整的字幕管理功能:
dart
// 添加字幕(自动排序)
void addSubtitle(SubtitleItem subtitle) {
subtitles.add(subtitle);
subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));
updatedAt = DateTime.now();
}
// 删除字幕
void removeSubtitle(String id) {
subtitles.removeWhere((subtitle) => subtitle.id == id);
updatedAt = DateTime.now();
}
// 更新字幕
void updateSubtitle(String id, SubtitleItem newSubtitle) {
final index = subtitles.indexWhere((subtitle) => subtitle.id == id);
if (index != -1) {
subtitles[index] = newSubtitle;
subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));
updatedAt = DateTime.now();
}
}
// 获取指定时间的激活字幕
List<SubtitleItem> getActiveSubtitles(Duration time) {
return subtitles.where((subtitle) => subtitle.isActiveAt(time)).toList();
}
播放器状态枚举
播放器状态管理播放控制:
dart
enum PlayerState {
stopped, // 停止
playing, // 播放中
paused // 暂停
}
应用主体结构
应用入口
dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '简易字幕制作器',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const SubtitleEditorHomePage(),
);
}
}
应用采用深紫色作为主题色,营造专业而现代的视觉效果。
主页面结构
主页面使用底部导航栏实现四个核心功能模块:
dart
class SubtitleEditorHomePage extends StatefulWidget {
const SubtitleEditorHomePage({super.key});
@override
State<SubtitleEditorHomePage> createState() => _SubtitleEditorHomePageState();
}
class _SubtitleEditorHomePageState extends State<SubtitleEditorHomePage>
with TickerProviderStateMixin {
int _selectedIndex = 0;
List<SubtitleProject> _projects = []; // 项目列表
SubtitleProject? _currentProject; // 当前项目
// 播放器相关
PlayerState _playerState = PlayerState.stopped;
Duration _currentTime = Duration.zero;
Duration _totalDuration = const Duration(minutes: 5);
Timer? _playTimer;
// 编辑相关
SubtitleItem? _selectedSubtitle;
final TextEditingController _textController = TextEditingController();
final TextEditingController _startTimeController = TextEditingController();
final TextEditingController _endTimeController = TextEditingController();
// 动画控制器
late AnimationController _timelineAnimationController;
late Animation<double> _timelineAnimation;
}
编辑功能实现
播放控制器
播放控制器是字幕编辑的核心组件:
dart
Widget _buildPlayerControls() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
children: [
// 时间显示
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatDuration(_currentTime),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
_formatDuration(_totalDuration),
style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
),
],
),
const SizedBox(height: 8),
// 进度条
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 4,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
),
child: Slider(
value: _currentTime.inMilliseconds.toDouble(),
max: _totalDuration.inMilliseconds.toDouble(),
onChanged: (value) {
setState(() {
_currentTime = Duration(milliseconds: value.toInt());
});
},
activeColor: Colors.deepPurple,
),
),
const SizedBox(height: 12),
// 播放控制按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: _skipBackward,
icon: const Icon(Icons.skip_previous),
iconSize: 32,
),
const SizedBox(width: 16),
Container(
decoration: BoxDecoration(
color: Colors.deepPurple,
shape: BoxShape.circle,
),
child: IconButton(
onPressed: _togglePlayPause,
icon: Icon(
_playerState == PlayerState.playing
? Icons.pause
: Icons.play_arrow,
color: Colors.white,
),
iconSize: 32,
),
),
const SizedBox(width: 16),
IconButton(
onPressed: _skipForward,
icon: const Icon(Icons.skip_next),
iconSize: 32,
),
],
),
],
),
);
}
播放控制逻辑
播放控制通过定时器实现时间更新:
dart
void _togglePlayPause() {
setState(() {
if (_playerState == PlayerState.playing) {
_playerState = PlayerState.paused;
_playTimer?.cancel();
} else {
_playerState = PlayerState.playing;
_startPlayback();
}
});
}
void _startPlayback() {
_playTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
setState(() {
_currentTime += const Duration(milliseconds: 100);
if (_currentTime >= _totalDuration) {
_currentTime = _totalDuration;
_playerState = PlayerState.stopped;
timer.cancel();
}
});
});
}
void _skipBackward() {
setState(() {
_currentTime = Duration(
milliseconds: (_currentTime.inMilliseconds - 5000).clamp(0, _totalDuration.inMilliseconds),
);
});
}
void _skipForward() {
setState(() {
_currentTime = Duration(
milliseconds: (_currentTime.inMilliseconds + 5000).clamp(0, _totalDuration.inMilliseconds),
);
});
}
字幕编辑器
字幕编辑器提供完整的字幕编辑功能:
dart
Widget _buildSubtitleEditor() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 编辑器标题和操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'字幕编辑',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Row(
children: [
ElevatedButton.icon(
onPressed: _addNewSubtitle,
icon: const Icon(Icons.add, size: 16),
label: const Text('新增'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
minimumSize: const Size(80, 32),
),
),
const SizedBox(width: 8),
if (_selectedSubtitle != null)
ElevatedButton.icon(
onPressed: _deleteSelectedSubtitle,
icon: const Icon(Icons.delete, size: 16),
label: const Text('删除'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
minimumSize: const Size(80, 32),
),
),
],
),
],
),
const SizedBox(height: 16),
// 字幕文本编辑
TextField(
controller: _textController,
decoration: const InputDecoration(
labelText: '字幕内容',
hintText: '请输入字幕文本...',
border: OutlineInputBorder(),
),
maxLines: 3,
onChanged: _updateSelectedSubtitleText,
),
const SizedBox(height: 16),
// 时间设置
Row(
children: [
Expanded(
child: TextField(
controller: _startTimeController,
decoration: const InputDecoration(
labelText: '开始时间',
hintText: '00:00:00',
border: OutlineInputBorder(),
),
onChanged: _updateStartTime,
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: _endTimeController,
decoration: const InputDecoration(
labelText: '结束时间',
hintText: '00:00:03',
border: OutlineInputBorder(),
),
onChanged: _updateEndTime,
),
),
],
),
const SizedBox(height: 16),
// 快速时间设置按钮
Wrap(
spacing: 8,
children: [
_buildQuickTimeButton('当前时间', () => _setCurrentTime(true)),
_buildQuickTimeButton('+3秒', () => _adjustTime(3)),
_buildQuickTimeButton('+5秒', () => _adjustTime(5)),
_buildQuickTimeButton('-1秒', () => _adjustTime(-1)),
],
),
const SizedBox(height: 16),
// 保存按钮
if (_selectedSubtitle != null)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _saveSubtitleChanges,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
),
child: const Text('保存修改'),
),
),
],
),
);
}
字幕编辑逻辑
字幕编辑的核心逻辑实现:
dart
void _addNewSubtitle() {
if (_currentProject == null) return;
final newSubtitle = SubtitleItem(
id: 'sub_${DateTime.now().millisecondsSinceEpoch}',
text: '新字幕',
startTime: _currentTime,
endTime: _currentTime + const Duration(seconds: 3),
);
setState(() {
_currentProject!.addSubtitle(newSubtitle);
_selectedSubtitle = newSubtitle;
_updateEditorFields();
});
}
void _selectSubtitle(SubtitleItem subtitle) {
setState(() {
_selectedSubtitle = subtitle;
_updateEditorFields();
});
}
void _updateEditorFields() {
if (_selectedSubtitle != null) {
_textController.text = _selectedSubtitle!.text;
_startTimeController.text = _formatDuration(_selectedSubtitle!.startTime);
_endTimeController.text = _formatDuration(_selectedSubtitle!.endTime);
}
}
void _saveSubtitleChanges() {
if (_selectedSubtitle == null || _currentProject == null) return;
setState(() {
_currentProject!.updateSubtitle(_selectedSubtitle!.id, _selectedSubtitle!);
});
_showMessage('字幕已保存');
}
时间轴功能
时间轴视图
时间轴提供可视化的字幕时间管理:
dart
Widget _buildTimeline() {
return Container(
height: 300,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
children: [
// 时间刻度
Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: List.generate(11, (index) {
final time = Duration(
milliseconds: (_totalDuration.inMilliseconds * index / 10).round(),
);
return Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 1,
height: 20,
color: Colors.grey.shade400,
),
const SizedBox(height: 4),
Text(
_formatDuration(time),
style: const TextStyle(fontSize: 10),
),
],
),
);
}),
),
),
// 字幕轨道
Expanded(
child: Stack(
children: [
// 背景网格
CustomPaint(
size: const Size(double.infinity, double.infinity),
painter: TimelineGridPainter(),
),
// 字幕条
...(_currentProject?.subtitles.map((subtitle) {
return _buildSubtitleBar(subtitle);
}) ?? []),
// 播放指针
Positioned(
left: (_currentTime.inMilliseconds / _totalDuration.inMilliseconds) *
(MediaQuery.of(context).size.width - 32) + 16,
top: 0,
bottom: 0,
child: Container(
width: 2,
color: Colors.red,
child: const Align(
alignment: Alignment.topCenter,
child: Icon(
Icons.play_arrow,
color: Colors.red,
size: 16,
),
),
),
),
],
),
),
],
),
);
}
字幕条组件
时间轴上的字幕条显示:
dart
Widget _buildSubtitleBar(SubtitleItem subtitle) {
final screenWidth = MediaQuery.of(context).size.width - 32;
final startPosition = (subtitle.startTime.inMilliseconds / _totalDuration.inMilliseconds) * screenWidth;
final width = (subtitle.duration.inMilliseconds / _totalDuration.inMilliseconds) * screenWidth;
final isSelected = _selectedSubtitle?.id == subtitle.id;
return Positioned(
left: startPosition + 16,
top: 60,
width: width.clamp(20.0, screenWidth),
height: 40,
child: GestureDetector(
onTap: () => _selectSubtitle(subtitle),
child: Container(
decoration: BoxDecoration(
color: isSelected ? Colors.deepPurple : Colors.blue.shade300,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isSelected ? Colors.deepPurple.shade700 : Colors.blue.shade500,
width: 1,
),
),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
subtitle.text,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
'${_formatDuration(subtitle.startTime)} - ${_formatDuration(subtitle.endTime)}',
style: const TextStyle(
color: Colors.white70,
fontSize: 8,
),
),
],
),
),
),
);
}
时间轴网格绘制
自定义绘制器实现时间轴网格:
dart
class TimelineGridPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.grey.shade300
..strokeWidth = 0.5;
// 绘制垂直网格线
for (int i = 0; i <= 10; i++) {
final x = (size.width * i / 10);
canvas.drawLine(
Offset(x, 0),
Offset(x, size.height),
paint,
);
}
// 绘制水平网格线
for (int i = 0; i <= 5; i++) {
final y = (size.height * i / 5);
canvas.drawLine(
Offset(0, y),
Offset(size.width, y),
paint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
预览功能
预览页面设计
预览页面提供实时的字幕效果预览:
dart
Widget _buildPreviewPage() {
if (_currentProject == null) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.play_circle_outline, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'请先创建或选择一个项目',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
],
),
);
}
final activeSubtitles = _currentProject!.getActiveSubtitles(_currentTime);
return Column(
children: [
// 预览区域
Expanded(
child: Container(
width: double.infinity,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12),
),
child: Stack(
children: [
// 背景
const Center(
child: Icon(
Icons.movie,
size: 64,
color: Colors.white24,
),
),
// 字幕显示
...activeSubtitles.map((subtitle) {
return Positioned.fill(
child: Align(
alignment: subtitle.alignment,
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: subtitle.backgroundColor.withValues(alpha: subtitle.opacity),
borderRadius: BorderRadius.circular(8),
),
child: Text(
subtitle.text,
style: subtitle.style,
textAlign: TextAlign.center,
),
),
),
);
}),
// 时间显示
Positioned(
top: 16,
left: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: Text(
_formatDuration(_currentTime),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
// 预览控制
Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 播放控制
_buildPlayerControls(),
const SizedBox(height: 16),
// 预览选项
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: _toggleFullscreen,
icon: const Icon(Icons.fullscreen),
label: const Text('全屏'),
),
ElevatedButton.icon(
onPressed: _showSubtitleSettings,
icon: const Icon(Icons.text_fields),
label: const Text('样式'),
),
ElevatedButton.icon(
onPressed: _exportVideo,
icon: const Icon(Icons.video_file),
label: const Text('导出'),
),
],
),
],
),
),
],
);
}
项目管理
项目列表页面
项目管理页面提供完整的项目管理功能:
dart
Widget _buildProjectsPage() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 项目管理头部
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'我的项目',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
ElevatedButton.icon(
onPressed: _createNewProject,
icon: const Icon(Icons.add),
label: const Text('新建项目'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 20),
// 项目统计
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.deepPurple.shade400, Colors.blue.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
_buildProjectStatCard('项目总数', '${_projects.length}'),
const SizedBox(width: 16),
_buildProjectStatCard('字幕总数', '${_getTotalSubtitles()}'),
],
),
),
const SizedBox(height: 24),
// 项目列表
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _projects.length,
itemBuilder: (context, index) {
final project = _projects[index];
final isActive = _currentProject?.id == project.id;
return Container(
margin: const EdgeInsets.only(bottom: 12),
child: Card(
elevation: isActive ? 4 : 1,
color: isActive ? Colors.deepPurple.shade50 : null,
child: ListTile(
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isActive ? Colors.deepPurple : Colors.grey.shade400,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.movie_creation,
color: Colors.white,
size: 24,
),
),
title: Text(
project.name,
style: TextStyle(
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${project.subtitles.length}条字幕 • ${_formatDuration(project.totalDuration)}'),
Text(
'更新于 ${_formatDateTime(project.updatedAt)}',
style: const TextStyle(fontSize: 12),
),
],
),
trailing: PopupMenuButton<String>(
onSelected: (value) => _handleProjectAction(value, project),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'open',
child: Row(
children: [
Icon(Icons.open_in_new, size: 16),
SizedBox(width: 8),
Text('打开'),
],
),
),
const PopupMenuItem(
value: 'rename',
child: Row(
children: [
Icon(Icons.edit, size: 16),
SizedBox(width: 8),
Text('重命名'),
],
),
),
const PopupMenuItem(
value: 'duplicate',
child: Row(
children: [
Icon(Icons.copy, size: 16),
SizedBox(width: 8),
Text('复制'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, size: 16, color: Colors.red),
SizedBox(width: 8),
Text('删除', style: TextStyle(color: Colors.red)),
],
),
),
],
),
onTap: () => _openProject(project),
),
),
);
},
),
],
),
);
}
项目创建功能
新建项目的对话框实现:
dart
void _createNewProject() {
showDialog(
context: context,
builder: (context) {
final nameController = TextEditingController();
final durationController = TextEditingController(text: '5:00');
return AlertDialog(
title: const Text('新建项目'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: '项目名称',
hintText: '请输入项目名称',
),
),
const SizedBox(height: 16),
TextField(
controller: durationController,
decoration: const InputDecoration(
labelText: '项目时长',
hintText: '格式: 分:秒 (如 5:00)',
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
final name = nameController.text.trim();
final durationStr = durationController.text.trim();
if (name.isEmpty) {
_showMessage('请输入项目名称');
return;
}
final duration = _parseDuration('00:$durationStr') ?? const Duration(minutes: 5);
final newProject = SubtitleProject(
id: 'project_${DateTime.now().millisecondsSinceEpoch}',
name: name,
totalDuration: duration,
subtitles: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
setState(() {
_projects.add(newProject);
_currentProject = newProject;
_totalDuration = duration;
_currentTime = Duration.zero;
_selectedSubtitle = null;
_clearEditorFields();
});
Navigator.of(context).pop();
_showMessage('项目创建成功');
},
child: const Text('创建'),
),
],
);
},
);
}
导出功能
多格式导出
应用支持多种字幕格式的导出:
dart
void _exportSubtitles() {
if (_currentProject == null || _currentProject!.subtitles.isEmpty) {
_showMessage('没有字幕可以导出');
return;
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('导出字幕'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.text_snippet),
title: const Text('SRT格式'),
subtitle: const Text('通用字幕格式'),
onTap: () {
Navigator.of(context).pop();
_exportToSRT();
},
),
ListTile(
leading: const Icon(Icons.code),
title: const Text('VTT格式'),
subtitle: const Text('Web视频字幕格式'),
onTap: () {
Navigator.of(context).pop();
_exportToVTT();
},
),
ListTile(
leading: const Icon(Icons.description),
title: const Text('TXT格式'),
subtitle: const Text('纯文本格式'),
onTap: () {
Navigator.of(context).pop();
_exportToTXT();
},
),
],
),
),
);
}
SRT格式导出
SRT是最常用的字幕格式:
dart
String _generateSRTContent() {
final buffer = StringBuffer();
final subtitles = List<SubtitleItem>.from(_currentProject!.subtitles);
subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));
for (int i = 0; i < subtitles.length; i++) {
final subtitle = subtitles[i];
buffer.writeln('${i + 1}');
buffer.writeln('${_formatSRTTime(subtitle.startTime)} --> ${_formatSRTTime(subtitle.endTime)}');
buffer.writeln(subtitle.text);
buffer.writeln();
}
return buffer.toString();
}
String _formatSRTTime(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes % 60;
final seconds = duration.inSeconds % 60;
final milliseconds = duration.inMilliseconds % 1000;
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')},${milliseconds.toString().padLeft(3, '0')}';
}
VTT格式导出
VTT格式适用于Web视频:
dart
String _generateVTTContent() {
final buffer = StringBuffer();
buffer.writeln('WEBVTT');
buffer.writeln();
final subtitles = List<SubtitleItem>.from(_currentProject!.subtitles);
subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));
for (final subtitle in subtitles) {
buffer.writeln('${_formatVTTTime(subtitle.startTime)} --> ${_formatVTTTime(subtitle.endTime)}');
buffer.writeln(subtitle.text);
buffer.writeln();
}
return buffer.toString();
}
String _formatVTTTime(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes % 60;
final seconds = duration.inSeconds % 60;
final milliseconds = duration.inMilliseconds % 1000;
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}.${milliseconds.toString().padLeft(3, '0')}';
}
TXT格式导出
TXT格式提供简单的文本输出:
dart
String _generateTXTContent() {
final buffer = StringBuffer();
final subtitles = List<SubtitleItem>.from(_currentProject!.subtitles);
subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));
for (final subtitle in subtitles) {
buffer.writeln('[${_formatDuration(subtitle.startTime)} - ${_formatDuration(subtitle.endTime)}]');
buffer.writeln(subtitle.text);
buffer.writeln();
}
return buffer.toString();
}
智能功能
自动排列字幕
自动调整字幕时间,避免重叠:
dart
void _autoArrangeSubtitles() {
if (_currentProject == null || _currentProject!.subtitles.isEmpty) return;
// 自动调整字幕时间,避免重叠
final subtitles = List<SubtitleItem>.from(_currentProject!.subtitles);
subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));
for (int i = 1; i < subtitles.length; i++) {
final current = subtitles[i];
final previous = subtitles[i - 1];
if (current.startTime < previous.endTime) {
// 调整当前字幕的开始时间
final newStartTime = previous.endTime + const Duration(milliseconds: 500);
final duration = current.duration;
subtitles[i] = current.copyWith(
startTime: newStartTime,
endTime: newStartTime + duration,
);
}
}
setState(() {
_currentProject!.subtitles = subtitles;
_currentProject!.updatedAt = DateTime.now();
});
_showMessage('字幕时间已自动调整');
}
工具函数
时间格式化
应用包含多种时间格式化函数:
dart
// 基本时间格式化(MM:SS)
String _formatDuration(Duration duration) {
final minutes = duration.inMinutes;
final seconds = duration.inSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
// 日期时间格式化
String _formatDateTime(DateTime dateTime) {
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
// 时间字符串解析
Duration? _parseDuration(String timeStr) {
try {
final parts = timeStr.split(':');
if (parts.length == 2) {
final minutes = int.parse(parts[0]);
final seconds = int.parse(parts[1]);
return Duration(minutes: minutes, seconds: seconds);
} else if (parts.length == 3) {
final hours = int.parse(parts[0]);
final minutes = int.parse(parts[1]);
final seconds = int.parse(parts[2]);
return Duration(hours: hours, minutes: minutes, seconds: seconds);
}
} catch (e) {
// 解析失败
}
return null;
}
项目总结
简易字幕制作器应用成功实现了完整的字幕制作工作流程,通过专业的界面设计和强大的编辑功能,为用户提供了高效的字幕制作工具。
技术亮点
- 可视化时间轴:直观的时间轴管理,支持拖拽和精确编辑
- 实时预览:即时预览字幕效果,所见即所得
- 多格式导出:支持SRT、VTT、TXT等主流格式
- 项目管理:完整的项目管理系统,支持多项目并行
- 智能排列:自动调整字幕时间,避免冲突
功能特色
- 专业的字幕编辑界面和工具
- 可视化时间轴管理系统
- 实时播放预览功能
- 多种字幕格式导出支持
- 完整的项目管理和版本控制
- 智能的字幕时间调整算法
扩展方向
- 视频集成:支持视频文件导入和同步播放
- 样式编辑:丰富的字幕样式和特效设置
- 语音识别:自动语音转字幕功能
- 协作功能:多人协作编辑和版本管理
- 云端同步:项目云端存储和跨设备同步
- 批量处理:批量字幕处理和格式转换
通过本教程的学习,你已经掌握了Flutter应用开发的高级技能,包括复杂UI设计、自定义绘制、文件处理和项目管理。这些技能可以应用到媒体处理、创作工具等专业应用的开发中。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net