|
|
|
@ -1,5 +1,5 @@ |
|
|
|
import 'dart:math'; |
|
|
|
|
|
|
|
import 'dart:async'; |
|
|
|
import 'dart:math' as math; |
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
@ -17,7 +17,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
|
onPopInvokedWithResult: (didPop, result) async { |
|
|
|
if (didPop) { |
|
|
|
// 在返回时执行保存操作 |
|
|
|
controller.saveTitleChange(); |
|
|
|
//controller.saveTitleChange(); |
|
|
|
//await controller.saveRecording(); |
|
|
|
} |
|
|
|
}, |
|
|
|
@ -108,17 +108,15 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
|
// 使用LayoutBuilder获取准确的屏幕宽度 |
|
|
|
final screenWidth = constraints.maxWidth; |
|
|
|
|
|
|
|
// 初始化最大点数 |
|
|
|
if (controller.maxPoints == 0) { |
|
|
|
controller.setMaxPoints(screenWidth); |
|
|
|
} |
|
|
|
// 初始化最大可见波形条数量 |
|
|
|
controller.setMaxVisibleBars(screenWidth); |
|
|
|
|
|
|
|
return Column( |
|
|
|
children: [ |
|
|
|
SizedBox(height: 24.h), |
|
|
|
// 专业级实时流动波形显示区域 |
|
|
|
// 实时流动波形显示区域 |
|
|
|
Container( |
|
|
|
height: 120.h, |
|
|
|
height: 200.h, |
|
|
|
margin: EdgeInsets.symmetric(horizontal: 16.w), |
|
|
|
decoration: BoxDecoration( |
|
|
|
gradient: LinearGradient( |
|
|
|
@ -136,7 +134,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
|
const Color(0xFFF8F9FA), |
|
|
|
], |
|
|
|
), |
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
//borderRadius: BorderRadius.circular(12.r), |
|
|
|
border: Border.all( |
|
|
|
color: isDarkMode |
|
|
|
? Colors.white.withValues(alpha: 0.1) |
|
|
|
@ -161,7 +159,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
|
_WaveformWidget( |
|
|
|
controller: controller, |
|
|
|
isDarkMode: isDarkMode, |
|
|
|
size: Size(screenWidth - 32.w, 120.h), |
|
|
|
size: Size(screenWidth - 32.w, 200.h), |
|
|
|
), |
|
|
|
|
|
|
|
// 录音状态指示器 |
|
|
|
@ -213,11 +211,14 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
|
SizedBox(height: 24.h), |
|
|
|
// 时间显示 - 只包装需要响应式更新的Text widget |
|
|
|
Obx(() => Text( |
|
|
|
controller.formatTime(controller.seconds.value), |
|
|
|
controller.formatTime(controller.milliseconds.value), |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 48.sp, |
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
fontWeight: FontWeight.normal, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
fontFeatures: const [ |
|
|
|
FontFeature.tabularFigures() |
|
|
|
], // 使用等宽数字,避免跳动 |
|
|
|
), |
|
|
|
)), |
|
|
|
const Spacer(), |
|
|
|
@ -449,7 +450,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 实时流动波形绘制器 |
|
|
|
/// 简化的实时流动波形绘制器 |
|
|
|
class RealTimeWaveformPainter extends CustomPainter { |
|
|
|
final List<double> waveData; |
|
|
|
final bool isDarkMode; |
|
|
|
@ -466,22 +467,19 @@ class RealTimeWaveformPainter extends CustomPainter { |
|
|
|
@override |
|
|
|
void paint(Canvas canvas, Size size) { |
|
|
|
final centerY = size.height / 2; |
|
|
|
final centerX = size.width / 2; |
|
|
|
|
|
|
|
// 绘制中央基准线 |
|
|
|
_drawCenterLine(canvas, size, centerX, centerY); |
|
|
|
_drawCenterLine(canvas, size, centerY); |
|
|
|
|
|
|
|
// 只在录音状态下且有波形数据时才绘制波形 |
|
|
|
if (isRecording && waveData.isNotEmpty) { |
|
|
|
_drawWaveform(canvas, size, centerX, centerY); |
|
|
|
_drawWaveform(canvas, size, centerY); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 绘制中央时间参考线(只保留水平线) |
|
|
|
void _drawCenterLine( |
|
|
|
Canvas canvas, Size size, double centerX, double centerY) { |
|
|
|
// 只绘制水平中线 |
|
|
|
final horizontalLinePaint = Paint() |
|
|
|
/// 绘制中央基准线 |
|
|
|
void _drawCenterLine(Canvas canvas, Size size, double centerY) { |
|
|
|
final linePaint = Paint() |
|
|
|
..color = |
|
|
|
(isDarkMode ? Colors.white : Colors.black).withValues(alpha: 0.15) |
|
|
|
..strokeWidth = 1.0; |
|
|
|
@ -489,134 +487,109 @@ class RealTimeWaveformPainter extends CustomPainter { |
|
|
|
canvas.drawLine( |
|
|
|
Offset(0, centerY), |
|
|
|
Offset(size.width, centerY), |
|
|
|
horizontalLinePaint, |
|
|
|
linePaint, |
|
|
|
); |
|
|
|
|
|
|
|
// 移除垂直时间参考线和三角形指示器 |
|
|
|
} |
|
|
|
|
|
|
|
/// 绘制实时流动波形(扩散到整个区域) |
|
|
|
void _drawWaveform(Canvas canvas, Size size, double centerX, double centerY) { |
|
|
|
final barWidth = 2.5; // 波形条宽度 |
|
|
|
final barSpacing = 0.5; // 波形条间距 |
|
|
|
final totalBarWidth = barWidth + barSpacing; |
|
|
|
/// 绘制简化的流动波形 |
|
|
|
void _drawWaveform(Canvas canvas, Size size, double centerY) { |
|
|
|
const double barWidth = 3.0; // 波形条宽度 |
|
|
|
const double barSpacing = 1.0; // 波形条间距 |
|
|
|
const double totalBarWidth = barWidth + barSpacing; |
|
|
|
|
|
|
|
// 计算整个宽度可以显示多少个波形条 |
|
|
|
// 计算可显示的波形条数量 |
|
|
|
final maxBars = (size.width / totalBarWidth).floor(); |
|
|
|
final visibleBars = waveData.length > maxBars ? maxBars : waveData.length; |
|
|
|
|
|
|
|
// 绘制扩散到整个区域的波形 |
|
|
|
_drawFullWaveform( |
|
|
|
canvas, size, centerY, barWidth, totalBarWidth, maxBars); |
|
|
|
} |
|
|
|
|
|
|
|
/// 绘制扩散到整个区域的波形 |
|
|
|
void _drawFullWaveform(Canvas canvas, Size size, double centerY, |
|
|
|
double barWidth, double totalBarWidth, int maxBars) { |
|
|
|
final dataLength = waveData.length; |
|
|
|
if (dataLength == 0) return; |
|
|
|
|
|
|
|
// 计算流动偏移量 |
|
|
|
// 计算流动偏移量(从右向左移动) |
|
|
|
final flowOffset = animationValue * totalBarWidth; |
|
|
|
|
|
|
|
// 绘制波形条,从左到右填满整个区域 |
|
|
|
final displayCount = min(dataLength, maxBars); |
|
|
|
|
|
|
|
for (int i = 0; i < displayCount; i++) { |
|
|
|
// 计算波形条的x位置:从左边开始排列,加上流动偏移 |
|
|
|
final x = i * totalBarWidth + flowOffset; |
|
|
|
|
|
|
|
// 如果波形条完全移出屏幕右侧,跳过绘制 |
|
|
|
if (x > size.width) continue; |
|
|
|
// 如果波形条完全移出屏幕左侧,跳过绘制 |
|
|
|
if (x + barWidth < 0) continue; |
|
|
|
|
|
|
|
// 获取对应的音频数据(循环使用数据以填满整个区域) |
|
|
|
final dataIndex = (dataLength - 1 - (i % dataLength)) % dataLength; |
|
|
|
// 从右到左绘制波形条 |
|
|
|
for (int i = 0; i < visibleBars; i++) { |
|
|
|
final dataIndex = waveData.length - 1 - i; |
|
|
|
final amplitude = waveData[dataIndex]; |
|
|
|
|
|
|
|
// 计算透明度:中央区域更亮,边缘渐暗 |
|
|
|
final distanceFromCenter = (x - size.width / 2).abs() / (size.width / 2); |
|
|
|
final opacity = (1.0 - distanceFromCenter * 0.4).clamp(0.3, 1.0); |
|
|
|
// 计算波形条的x位置(从右侧开始,向左排列) |
|
|
|
final x = size.width - (i + 1) * totalBarWidth + flowOffset; |
|
|
|
|
|
|
|
// 跳过屏幕外的波形条 |
|
|
|
if (x < -barWidth || x > size.width) continue; |
|
|
|
|
|
|
|
_drawWaveBar(canvas, x, centerY, barWidth, amplitude, opacity); |
|
|
|
_drawWaveBar( |
|
|
|
canvas, x, centerY, amplitude, i.toDouble(), visibleBars.toDouble()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 绘制单个上下对称的波形条 |
|
|
|
void _drawWaveBar(Canvas canvas, double x, double centerY, double barWidth, |
|
|
|
double amplitude, double opacity) { |
|
|
|
// 将振幅转换为显示高度(上下对称) |
|
|
|
final maxHeight = 50.0; // 最大高度 |
|
|
|
final barHeight = (amplitude / 100.0) * maxHeight; |
|
|
|
/// 绘制单个波形条 |
|
|
|
void _drawWaveBar(Canvas canvas, double x, double centerY, double amplitude, |
|
|
|
double index, double totalBars) { |
|
|
|
const double barWidth = 2.0; |
|
|
|
const double maxHeight = 200.0; |
|
|
|
const double minHeight = 4.0; // 最小基础高度 |
|
|
|
const double containerPadding = 10.0; // 容器内边距,防止超出边界 |
|
|
|
|
|
|
|
// 创建上下对称的圆角矩形 |
|
|
|
// 计算可用的最大高度(考虑容器边界) |
|
|
|
final availableHeight = (centerY - containerPadding) * 2; // 上下对称 |
|
|
|
final effectiveMaxHeight = math.min(maxHeight, availableHeight); |
|
|
|
|
|
|
|
// 优化波形条高度计算:确保有基础高度 + 振幅变化 |
|
|
|
final normalizedAmplitude = (amplitude / 100.0).clamp(0.0, 1.0); |
|
|
|
final dynamicHeight = |
|
|
|
normalizedAmplitude * (effectiveMaxHeight - minHeight); |
|
|
|
final barHeight = minHeight + dynamicHeight; |
|
|
|
|
|
|
|
// 计算透明度(越新的波形条越不透明) |
|
|
|
//final opacity = (1.0 - (index / totalBars) * 0.6).clamp(0.3, 1.0); |
|
|
|
|
|
|
|
// 创建波形条矩形 |
|
|
|
final rect = RRect.fromRectAndRadius( |
|
|
|
Rect.fromCenter( |
|
|
|
center: Offset(x + barWidth / 2, centerY), |
|
|
|
width: barWidth, |
|
|
|
height: barHeight * 2, // 上下对称,总高度是barHeight的2倍 |
|
|
|
height: barHeight, // 使用优化后的高度 |
|
|
|
), |
|
|
|
Radius.circular(barWidth / 2), |
|
|
|
const Radius.circular(1.5), |
|
|
|
); |
|
|
|
|
|
|
|
// 创建渐变效果 |
|
|
|
final gradient = LinearGradient( |
|
|
|
colors: _getGradientColors(opacity), |
|
|
|
begin: Alignment.topCenter, |
|
|
|
end: Alignment.bottomCenter, |
|
|
|
); |
|
|
|
// 创建渐变颜色 |
|
|
|
final Color baseColor = isDarkMode |
|
|
|
? Colors.grey[500]! // 深灰色 |
|
|
|
: Colors.grey[600]!; // 更深灰色 |
|
|
|
|
|
|
|
final paint = Paint() |
|
|
|
..shader = gradient.createShader(rect.outerRect) |
|
|
|
..color = baseColor |
|
|
|
..style = PaintingStyle.fill; |
|
|
|
|
|
|
|
canvas.drawRRect(rect, paint); |
|
|
|
} |
|
|
|
|
|
|
|
// 可选:添加高光效果(仅对较新的波形条) |
|
|
|
if (opacity > 0.7) { |
|
|
|
final highlightRect = RRect.fromRectAndRadius( |
|
|
|
Rect.fromCenter( |
|
|
|
center: Offset(x + barWidth / 2, centerY), |
|
|
|
width: barWidth * 0.4, |
|
|
|
height: barHeight * 2 * 0.8, |
|
|
|
), |
|
|
|
Radius.circular(barWidth / 4), |
|
|
|
); |
|
|
|
|
|
|
|
final highlightPaint = Paint() |
|
|
|
..color = Colors.white.withValues(alpha: 0.2 * opacity) |
|
|
|
..style = PaintingStyle.fill; |
|
|
|
@override |
|
|
|
bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) { |
|
|
|
// 只有在以下情况才重绘 |
|
|
|
if (oldDelegate.isDarkMode != isDarkMode || |
|
|
|
oldDelegate.isRecording != isRecording) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
canvas.drawRRect(highlightRect, highlightPaint); |
|
|
|
// 数据长度变化才重绘 |
|
|
|
if (oldDelegate.waveData.length != waveData.length) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 获取渐变颜色 |
|
|
|
List<Color> _getGradientColors(double opacity) { |
|
|
|
return isDarkMode |
|
|
|
? [ |
|
|
|
Colors.blue[400]!.withValues(alpha: opacity), |
|
|
|
Colors.blue[200]!.withValues(alpha: opacity * 0.7), |
|
|
|
Colors.blue[100]!.withValues(alpha: opacity * 0.4), |
|
|
|
] |
|
|
|
: [ |
|
|
|
Colors.blue[500]!.withValues(alpha: opacity), |
|
|
|
Colors.blue[300]!.withValues(alpha: opacity * 0.7), |
|
|
|
Colors.blue[100]!.withValues(alpha: opacity * 0.4), |
|
|
|
]; |
|
|
|
} |
|
|
|
// 数据内容变化(比较最后一个值) |
|
|
|
if (waveData.isNotEmpty && |
|
|
|
oldDelegate.waveData.isNotEmpty && |
|
|
|
(waveData.last - oldDelegate.waveData.last).abs() > 0.1) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
@override |
|
|
|
bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) { |
|
|
|
return oldDelegate.waveData != waveData || |
|
|
|
oldDelegate.isDarkMode != isDarkMode || |
|
|
|
oldDelegate.isRecording != isRecording || |
|
|
|
oldDelegate.animationValue != animationValue; |
|
|
|
// 动画值每 0.1 变化才重绘(避免每帧都重绘) |
|
|
|
return (animationValue - oldDelegate.animationValue).abs() > 0.1; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 专门的波形Widget,正确管理GetX响应式状态 |
|
|
|
class _WaveformWidget extends StatelessWidget { |
|
|
|
/// 优化的波形Widget,避免双重响应式更新 |
|
|
|
class _WaveformWidget extends StatefulWidget { |
|
|
|
final MeetingRecordController controller; |
|
|
|
final bool isDarkMode; |
|
|
|
final Size size; |
|
|
|
@ -627,25 +600,67 @@ class _WaveformWidget extends StatelessWidget { |
|
|
|
required this.size, |
|
|
|
}); |
|
|
|
|
|
|
|
@override |
|
|
|
State<_WaveformWidget> createState() => _WaveformWidgetState(); |
|
|
|
} |
|
|
|
|
|
|
|
class _WaveformWidgetState extends State<_WaveformWidget> { |
|
|
|
late List<double> _waveData; |
|
|
|
late bool _isRecording; |
|
|
|
StreamSubscription? _waveDataSubscription; |
|
|
|
StreamSubscription? _isRecordingSubscription; |
|
|
|
|
|
|
|
@override |
|
|
|
void initState() { |
|
|
|
super.initState(); |
|
|
|
_waveData = List.from(widget.controller.waveData); |
|
|
|
_isRecording = widget.controller.isRecording.value; |
|
|
|
|
|
|
|
// 监听 waveData 变化 |
|
|
|
_waveDataSubscription = widget.controller.waveData.listen((data) { |
|
|
|
if (mounted) { |
|
|
|
setState(() { |
|
|
|
_waveData = List.from(data); |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 监听 isRecording 变化 |
|
|
|
_isRecordingSubscription = widget.controller.isRecording.listen((value) { |
|
|
|
if (mounted) { |
|
|
|
setState(() { |
|
|
|
_isRecording = value; |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
@override |
|
|
|
void dispose() { |
|
|
|
_waveDataSubscription?.cancel(); |
|
|
|
_isRecordingSubscription?.cancel(); |
|
|
|
super.dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
@override |
|
|
|
Widget build(BuildContext context) { |
|
|
|
// 使用AnimatedBuilder监听动画变化 |
|
|
|
return AnimatedBuilder( |
|
|
|
animation: controller.waveformAnimation, |
|
|
|
builder: (context, child) { |
|
|
|
// 使用Obx只监听需要响应式更新的数据 |
|
|
|
return Obx(() { |
|
|
|
return RepaintBoundary( |
|
|
|
child: AnimatedBuilder( |
|
|
|
animation: widget.controller.waveformAnimation, |
|
|
|
builder: (context, child) { |
|
|
|
return CustomPaint( |
|
|
|
size: widget.size, |
|
|
|
painter: RealTimeWaveformPainter( |
|
|
|
waveData: controller.waveData, |
|
|
|
isDarkMode: isDarkMode, |
|
|
|
isRecording: controller.isRecording.value, |
|
|
|
animationValue: controller.waveformAnimation.value, |
|
|
|
waveData: _waveData, |
|
|
|
isDarkMode: widget.isDarkMode, |
|
|
|
isRecording: _isRecording, |
|
|
|
animationValue: widget.controller.waveformAnimation.value, |
|
|
|
), |
|
|
|
size: size, |
|
|
|
isComplex: true, // 告诉 Flutter 这个绘制较复杂,可缓存 |
|
|
|
willChange: false, // 数据不会频繁变化(我们已优化) |
|
|
|
); |
|
|
|
}); |
|
|
|
}, |
|
|
|
}, |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|