Browse Source

refactor(meeting): 优化会议记录控制器的音频处理和波形显示

- 重构音频数据处理逻辑,使用帧间隔和采样控制提升性能
- 改进波形显示算法,增加毫秒级时间精度
- 简化波形绘制逻辑,优化重绘性能
- 添加数据更新控制器统一管理动画和数据处理
weicu
lxm 1 year ago
parent
commit
0748414534
  1. 246
      lib/modules/meeting/controllers/meeting_record_controller.dart
  2. 271
      lib/modules/meeting/views/meeting_record_view.dart

246
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -13,27 +13,28 @@ import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/ble_manager.dart';
/// Enumerates the possible states of the recording timer
/// 会议记录状态枚举
enum TimerStatus {
stopped, // Timer is inactive
running, // Timer is actively counting
paused, // Timer is paused
stopped, // 停止
running, // 运行中
paused, // 暂停
}
/// Controller for meeting recording screen
/// Handles recording state, audio processing, transcription, and UI interactions
/// 会议记录控制器
/// 负责会议记录的状态管理、音频处理、语音识别和UI交互
class MeetingRecordController extends GetxController
with GetSingleTickerProviderStateMixin {
with GetTickerProviderStateMixin {
// Services
final AsrService _asrService = Get.find<AsrService>();
final BleManager _bleManager = Get.find<BleManager>();
// Stream subscriptions
// 流订阅
StreamSubscription? _recognitionSubscription;
// Recording state
// 记录状态
final Rx<TimerStatus> timerStatus = TimerStatus.stopped.obs;
final RxInt seconds = 0.obs;
final RxInt milliseconds = 0.obs;
final RxInt audioType = 0.obs; // 0=Live, 1=Media, 2=Call
final RxBool isMarked = false.obs;
final RxBool showTextContent = false.obs;
@ -60,19 +61,29 @@ class MeetingRecordController extends GetxController
// 移除了未使用的变量和TabController
StreamSubscription<Uint8List>? _audioDataSubscription;
// 添加波形放大倍数变量
final double waveAmplifyFactor = 250.0; // 增加波动幅度
final RxDouble ursorPosition = 0.0.obs; // 当前播放位置
final RxInt _currentPosition = 0.obs; // 原始位置索引
int maxPoints = 300; // 最大显示点数(根据屏幕宽度优化)
int sampleRate = 16000; // 假设音频采样率(根据实际情况调整)
// 简化的音波数据管理
final int frameIntervalMs = 300; // 300ms间隔,更稳定的帧率
int maxVisibleBars = 60; // 最大可见波形条数量
Timer? _audioFrameTimer; // 音频帧定时器
double _currentFrameAmplitude = 0.0; // 当前帧累积振幅
int _currentFrameSamples = 0; // 当前帧样本数量
// 性能优化:音频处理采样控制
int _audioProcessCounter = 0;
static const int _audioProcessInterval = 3; // 每3个音频包处理1次
// 波形流动动画控制器
late AnimationController waveformAnimationController;
late Animation<double> waveformAnimation;
// 设置最大点数(根据屏幕宽度)
void setMaxPoints(double screenWidth) {
maxPoints = screenWidth ~/ 3; // 增加数据点密度以填满整个区域
// 数据更新控制器 - 统一管理数据更新和动画
late AnimationController _dataUpdateController;
late Animation<double> _dataUpdateAnimation;
// 设置最大可见波形条数量(根据屏幕宽度)
void setMaxVisibleBars(double screenWidth) {
const barWidth = 3.0; // 波形条宽度
const barSpacing = 1.0; // 波形条间距
maxVisibleBars = (screenWidth / (barWidth + barSpacing)).floor();
}
@override
@ -87,7 +98,9 @@ class MeetingRecordController extends GetxController
@override
void onClose() {
_cleanupResources();
// 清理两个AnimationController
waveformAnimationController.dispose();
_dataUpdateController.dispose();
super.onClose();
}
@ -121,11 +134,36 @@ class MeetingRecordController extends GetxController
titleFocusNode.addListener(_handleTitleFocusChange);
}
/// 初始化波形流动动画
/// 初始化波形流动动画和数据更新控制器
void _initializeWaveformAnimation() {
// 设置动画持续时间,控制流动速度
// 1. 数据更新控制器 - 控制数据提交频率
_dataUpdateController = AnimationController(
duration: Duration(milliseconds: frameIntervalMs), // 250ms 一次数据更新
vsync: this,
);
_dataUpdateAnimation =
Tween<double>(begin: 0.0, end: 1.0).animate(_dataUpdateController);
// 监听数据更新动画,每个周期结束时提交数据
_dataUpdateAnimation.addListener(() {
if (!isRecording.value) return;
// 每个动画周期结束时,才提交一次数据
if (_dataUpdateController.value >= 1.0) {
_commitAudioFrame();
// 重置动画,继续循环
_dataUpdateController.reset();
if (isRecording.value) {
_dataUpdateController.forward();
}
}
});
// 2. 波形流动动画控制器 - 控制视觉流动效果
waveformAnimationController = AnimationController(
duration: const Duration(milliseconds: 100), // 更快的流动速度
duration: Duration(milliseconds: frameIntervalMs), // 与数据更新同步
vsync: this,
);
@ -169,6 +207,8 @@ class MeetingRecordController extends GetxController
/// Cleans up resources when controller is disposed
void _cleanupResources() {
_timer?.cancel();
_audioFrameTimer?.cancel(); // 清理音频帧定时器
// AnimationController在onClose中统一清理
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
_audioDataSubscription?.cancel();
@ -217,10 +257,17 @@ class MeetingRecordController extends GetxController
/// Toggles important mark status
void toggleMarked() => isMarked.toggle();
/// Formats seconds to MM:SS display format
String formatTime(int seconds) {
return '${(seconds ~/ 60).toString().padLeft(2, '0')}'
':${(seconds % 60).toString().padLeft(2, '0')}';
/// 格式化时间为 MM:SS.S 显示格式
String formatTime(int milliseconds) {
// 使用毫秒级精度计算
final totalMs = milliseconds;
final minutes = (totalMs ~/ 60000);
final secs = ((totalMs % 60000) ~/ 1000);
final centiseconds = ((totalMs % 1000) ~/ 100); // 取十分之一秒(一位小数)
return '${minutes.toString().padLeft(2, '0')}'
':${secs.toString().padLeft(2, '0')}'
'.${centiseconds.toString()}';
}
/// Starts/resumes recording based on current state
@ -263,7 +310,7 @@ class MeetingRecordController extends GetxController
await _asrService.enableRecord("${dir.path}$fullFileName.wav",
acceptAudioData: true);
// 监听音频数据流
// 通过 asrService 获取音频数据流
_audioDataSubscription =
_asrService.getAudioDataStream()?.listen((audioData) {
// 处理音频数据,例如:
@ -282,8 +329,9 @@ class MeetingRecordController extends GetxController
newName.value = fullFileName;
titleEditingController.text = fullFileName;
// 启动波形动画
// 启动波形动画和音频帧定时器
_startWaveformAnimation();
_startAudioFrameTimer();
// Start hardware components
_startHardwareServices();
@ -311,6 +359,7 @@ class MeetingRecordController extends GetxController
Future<void> _pauseRecording() async {
await _asrService.pauseRecord();
_stopWaveformAnimation(); // 暂停时停止动画
_stopAudioFrameTimer(); // 停止音频帧定时器
pauseTimer();
}
@ -318,88 +367,93 @@ class MeetingRecordController extends GetxController
Future<void> _resumeRecording() async {
await _asrService.resumeRecord();
_startWaveformAnimation(); // 恢复时启动动画
_startAudioFrameTimer(); // 重启音频帧定时器
startTimer();
}
/// 处理原始Uint8List音频数据
/// 处理原始Uint8List音频数据 - 只累积,不更新UI,性能优化
void _processAudioData(Uint8List audioData) {
// 将Uint8List转换为Float32List(PCM16 -> 浮点)
final pcmData = _convertToPCM(audioData);
if (!isRecording.value) return;
// 降采样和添加数据
_addAudioData(pcmData);
// 性能优化:不是每个音频包都处理,减少CPU占用
_audioProcessCounter++;
if (_audioProcessCounter % _audioProcessInterval != 0) {
return; // 跳过这次处理
}
// 将音频数据转换为振幅值并累积到当前帧
final amplitude = _calculateAmplitude(audioData);
_currentFrameAmplitude += amplitude;
_currentFrameSamples++;
}
/// 转换Uint8List为PCM浮点数组
Float32List _convertToPCM(Uint8List uint8List) {
// 假设音频为16位PCM小端格式
final byteData = ByteData.sublistView(uint8List);
final length = uint8List.length ~/ 2;
final pcmData = Float32List(length);
for (int i = 0; i < length; i++) {
// 读取16位有符号整数 (-32768 到 32767)
final int intValue = byteData.getInt16(i * 2, Endian.little);
// 转换为浮点数 (-1.0 到 1.0)
pcmData[i] = intValue / 32768.0;
/// 提交音频帧数据到波形显示
void _commitAudioFrame() {
// 计算当前帧的平均振幅
double frameAmplitude = 0.0;
if (_currentFrameSamples > 0) {
frameAmplitude = _currentFrameAmplitude / _currentFrameSamples;
}
return pcmData;
}
// 转换为显示数据
final newAmplitude = frameAmplitude * 800.0;
// 更新波形数据
if (waveData.length >= maxVisibleBars) {
waveData.removeAt(0);
}
waveData.add(newAmplitude);
/// 降采样音频数据
List<double> _downsampleAudio(Float32List audio) {
if (audio.isEmpty) return [];
// 重置当前帧数据
_currentFrameAmplitude = 0.0;
_currentFrameSamples = 0;
}
final result = <double>[];
final step = (audio.length / maxPoints).ceil();
/// 计算音频数据的振幅
double _calculateAmplitude(Uint8List audioData) {
if (audioData.isEmpty) return 0.0;
// 计算每个区间的峰值(保留更多细节)
for (int i = 0; i < audio.length; i += step) {
double maxVal = 0.0;
final end = min(i + step, audio.length);
// 使用RMS (Root Mean Square) 算法,更准确反映音频能量
double sumSquares = 0.0;
int sampleCount = 0;
for (int j = i; j < end; j++) {
final absVal = audio[j].abs();
if (absVal > maxVal) maxVal = absVal;
for (int i = 0; i < audioData.length; i += 4) {
// 跳跃采样,减少计算量
if (i + 1 < audioData.length) {
final int sample = (audioData[i + 1] << 8) | audioData[i];
final int signedSample = sample > 32767 ? sample - 65536 : sample;
sumSquares += (signedSample * signedSample);
sampleCount++;
}
// 应用放大倍数增强波动效果
result.add(maxVal * waveAmplifyFactor);
}
return result;
if (sampleCount == 0) return 0.0;
final rms = sqrt(sumSquares / sampleCount);
return (rms / 32768.0).clamp(0.0, 1.0);
}
// 获取当前光标位置(0.0 - 1.0)
double get cursorPositionValue {
if (waveData.isEmpty) return 0.0;
return _currentPosition.value / waveData.length;
}
/// 启动数据更新控制器 - 替代Timer
void _startAudioFrameTimer() {
_audioFrameTimer?.cancel(); // 清理旧的Timer(如果有)
_audioFrameTimer = null;
/// 添加处理后的音频数据 - 优化全屏流动效果
void _addAudioData(Float32List newAudio) {
if (!isRecording.value) return;
// 1. 降采样处理
final downsampled = _downsampleAudio(newAudio);
if (downsampled.isEmpty) return;
// 启动数据更新控制器
_dataUpdateController.forward();
}
// 2. 逐个添加数据点,确保平滑流动
for (final sample in downsampled) {
waveData.add(sample);
/// 停止数据更新控制器
void _stopAudioFrameTimer() {
_audioFrameTimer?.cancel();
_audioFrameTimer = null;
// 维护合理的数据长度 - 保持足够的数据以填满整个屏幕
if (waveData.length > maxPoints * 2) {
waveData.removeAt(0); // 移除最旧的数据
}
}
// 停止数据更新控制器
_dataUpdateController.stop();
_dataUpdateController.reset();
// 3. 确保动画持续运行
_ensureAnimationRunning();
// 4. 更新位置信息
_currentPosition.value = waveData.length;
// 重置当前帧数据和计数器
_currentFrameAmplitude = 0.0;
_currentFrameSamples = 0;
_audioProcessCounter = 0; // 重置音频处理计数器
}
/// 启动波形流动动画
@ -415,17 +469,11 @@ class MeetingRecordController extends GetxController
waveformAnimationController.reset();
}
/// 确保动画持续运行
void _ensureAnimationRunning() {
if (isRecording.value && !waveformAnimationController.isAnimating) {
waveformAnimationController.forward();
}
}
/// Saves recording and cleans up resources
Future<void> saveRecording() async {
resetTimer();
_stopWaveformAnimation(); // 停止波形动画
_stopAudioFrameTimer(); // 停止音频帧定时器
if (audioType.value != 0) {
_bleManager.closeCodec();
}
@ -477,19 +525,22 @@ class MeetingRecordController extends GetxController
/// TIMER CONTROL SECTION ///
/// Starts or resumes timer
/// 启动定时器
void startTimer() {
if (timerStatus.value != TimerStatus.running) {
timerStatus.value = TimerStatus.running;
_lastStartTime = DateTime.now().millisecondsSinceEpoch;
_timer = Timer.periodic(const Duration(milliseconds: 200), (_) {
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
// 改为100ms更新频率
if (timerStatus.value != TimerStatus.running) return;
final currentTime = DateTime.now().millisecondsSinceEpoch;
_elapsedMilliseconds += currentTime - _lastStartTime!;
_lastStartTime = currentTime;
seconds.value = (_elapsedMilliseconds / 1000).round();
milliseconds.value = _elapsedMilliseconds; // 更新毫秒值
});
}
}
@ -527,7 +578,4 @@ class MeetingRecordController extends GetxController
return baseHeight + rand * 10;
});
}
/// Current playback position in waveform (0-1)
double get cursorPosition => _currentPosition / waveData.length;
}

271
lib/modules/meeting/views/meeting_record_view.dart

@ -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, // 数据不会频繁变化(我们已优化)
);
});
},
},
),
);
}
}

Loading…
Cancel
Save