import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_decode_plugin/video_decode_plugin.dart'; // 用于存储H264文件中解析出的帧 class H264Frame { final Uint8List data; final int frameType; // 0=I帧, 1=P帧 final int? refIFrameSeq; H264Frame(this.data, this.frameType, [this.refIFrameSeq]); } // H264 NAL 单元类型 class NalUnitType { static const int UNSPECIFIED = 0; static const int CODED_SLICE_NON_IDR = 1; // P帧 static const int PARTITION_A = 2; static const int PARTITION_B = 3; static const int PARTITION_C = 4; static const int CODED_SLICE_IDR = 5; // I帧 static const int SEI = 6; // 补充增强信息 static const int SPS = 7; // 序列参数集 static const int PPS = 8; // 图像参数集 static const int AUD = 9; // 访问单元分隔符 static const int END_SEQUENCE = 10; // 序列结束 static const int END_STREAM = 11; // 码流结束 static const int FILLER_DATA = 12; // 填充数据 static const int SPS_EXT = 13; // SPS扩展 static const int PREFIX_NAL = 14; // 前缀NAL单元 static const int SUBSET_SPS = 15; // 子集SPS static const int DEPTH_PARAM_SET = 16; // 深度参数集 static const int RESERVED_17 = 17; // 保留类型 static const int RESERVED_18 = 18; // 保留类型 static const int CODED_SLICE_AUX = 19; // 辅助切片 static const int CODED_SLICE_EXTENSION = 20; // 扩展切片 static const int CODED_SLICE_DEPTH = 21; // 深度扩展 static const int RESERVED_22 = 22; // 保留类型 static const int RESERVED_23 = 23; // 保留类型 static const int STAP_A = 24; // 单时间聚合包A static const int STAP_B = 25; // 单时间聚合包B static const int MTAP_16 = 26; // 多时间聚合包16 static const int MTAP_24 = 27; // 多时间聚合包24 static const int FU_A = 28; // 分片单元A static const int FU_B = 29; // 分片单元B static const int RESERVED_30 = 30; // 保留类型 static const int RESERVED_31 = 31; // 保留类型 // 帧类型别名,方便调用 static const int NON_IDR_PICTURE = CODED_SLICE_NON_IDR; // P帧 static const int IDR_PICTURE = CODED_SLICE_IDR; // I帧 // 获取类型名称 static String getName(int type) { switch (type) { case UNSPECIFIED: return "未指定"; case CODED_SLICE_NON_IDR: return "P帧"; case PARTITION_A: return "分区A"; case PARTITION_B: return "分区B"; case PARTITION_C: return "分区C"; case CODED_SLICE_IDR: return "I帧"; case SEI: return "SEI"; case SPS: return "SPS"; case PPS: return "PPS"; case AUD: return "AUD"; case END_SEQUENCE: return "序列结束"; case END_STREAM: return "码流结束"; case FILLER_DATA: return "填充数据"; case SPS_EXT: return "SPS扩展"; case PREFIX_NAL: return "前缀NAL"; case SUBSET_SPS: return "子集SPS"; case CODED_SLICE_AUX: return "辅助切片"; case CODED_SLICE_EXTENSION: return "扩展切片"; case FU_A: return "分片单元A"; case FU_B: return "分片单元B"; default: return "未知($type)"; } } } void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: '视频解码演示', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const VideoView(), ); } } class VideoView extends StatefulWidget { const VideoView({Key? key}) : super(key: key); @override State createState() => _VideoViewState(); } /// 视频解码主页面的状态管理类。 /// 负责: /// 1. 加载和解析H264文件 /// 2. 初始化、释放视频解码器 /// 3. 控制视频播放、停止、帧解码流程 /// 4. 管理UI刷新与日志显示 /// 5. 维护与解码相关的核心状态(如纹理ID、播放状态、错误信息等) /// /// 主要成员说明: /// - _textureId: 当前解码器绑定的纹理ID /// - _isInitialized: 解码器是否已初始化 /// - _isPlaying: 是否正在播放 /// - _statusText: 当前状态文本 /// - _error: 错误信息 /// - _h264FileData: 加载的H264文件数据 /// - _h264Frames: 解析出的帧列表 /// - _currentFrameIndex: 当前播放帧索引 /// - _frameTimer: 帧播放定时器 /// - _logs: 日志信息 /// - _logScrollController: 日志滚动控制器 class _VideoViewState extends State { // 解码器状态 int? _textureId; bool _isInitialized = false; bool _isPlaying = false; String _statusText = "未初始化"; String _error = ""; // 帧统计 int _renderedFrameCount = 0; DateTime? _lastFrameTime; double _fps = 0; double _decoderFps = 0; // 解码器内部计算的FPS // 用于刷新解码器统计信息的定时器 Timer? _statsTimer; // 丢包模拟相关 bool _enablePacketLoss = false; double _packetLossRate = 0.1; // 默认10%丢包率 bool _dropIFrames = false; bool _dropPFrames = false; bool _dropSPSPPS = false; bool _burstPacketLossMode = false; int _burstPacketLossCounter = 0; int _droppedFramesCount = 0; // H264文件解析 Uint8List? _h264FileData; List _h264Frames = []; int _currentFrameIndex = 0; // 解码定时器 Timer? _frameTimer; // 日志 final List _logs = []; final ScrollController _logScrollController = ScrollController(); // 视频显示相关的属性 bool _showingErrorFrame = false; Timer? _errorFrameResetTimer; // 额外的解码器属性 int _totalFrames = 0; int _droppedFrames = 0; bool _hasSentIDR = false; bool _hasSentSPS = false; bool _hasSentPPS = false; @override void initState() { super.initState(); _loadH264File(); // 仅在需要时使用定时器更新一些UI元素 _statsTimer = Timer.periodic(Duration(milliseconds: 1000), (timer) { if (mounted) { setState(() { // 更新UI元素,例如帧率计算等 // 解码器统计信息现在通过回调获取,不需要在这里请求 }); } }); } @override void dispose() { _stopPlaying(); _releaseDecoder(); _frameTimer?.cancel(); _statsTimer?.cancel(); super.dispose(); } // 加载H264文件 Future _loadH264File() async { try { _log("正在加载 demo.h264 文件..."); final ByteData data = await rootBundle.load('assets/demo.h264'); setState(() { _h264FileData = data.buffer.asUint8List(); }); _log("H264文件加载完成: ${_h264FileData!.length} 字节"); // 解析H264文件 _parseH264File(); } catch (e) { _log("加载H264文件失败: $e"); setState(() { _error = "加载H264文件失败: $e"; }); } } // 解析H264文件,提取NAL单元 void _parseH264File() { if (_h264FileData == null) return; _log("开始解析H264文件..."); List frames = []; int startIndex = 0; while (startIndex < _h264FileData!.length - 4) { int nextStartIndex = _findStartCode(_h264FileData!, startIndex + 3); if (nextStartIndex == -1) { nextStartIndex = _h264FileData!.length; } int skipBytes = (_h264FileData![startIndex] == 0x00 && _h264FileData![startIndex + 1] == 0x00 && _h264FileData![startIndex + 2] == 0x00 && _h264FileData![startIndex + 3] == 0x01) ? 4 : 3; if (nextStartIndex > startIndex + skipBytes) { int nalType = _h264FileData![startIndex + skipBytes] & 0x1F; var nalData = Uint8List(nextStartIndex - startIndex); for (int i = 0; i < nalData.length; i++) { nalData[i] = _h264FileData![startIndex + i]; } // 0=I帧, 1=P帧 if (nalType == 7 || nalType == 8 || nalType == 5) { frames.add(H264Frame(nalData, 0)); } else { frames.add(H264Frame(nalData, 1)); } } startIndex = nextStartIndex; } setState(() { _h264Frames = frames; }); _log("H264文件解析完成,找到 " + frames.length.toString() + " 个帧"); } // 查找起始码的辅助方法 int _findStartCode(Uint8List data, int offset) { for (int i = offset; i < data.length - 3; i++) { // 检查是否为0x000001 if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01) { return i; } // 检查是否为0x00000001 if (i < data.length - 4 && data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x00 && data[i + 3] == 0x01) { return i; } } return -1; } // 获取NAL类型 int _getNalType(Uint8List data) { // 跳过起始码后再获取NAL单元类型 if (data.length > 4) { // 检查是否有0x00000001的起始码 if (data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01) { // 起始码后的第一个字节的低5位是NAL类型 if (data.length > 4) { return data[4] & 0x1F; } } // 检查是否有0x000001的起始码 else if (data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01) { // 起始码后的第一个字节的低5位是NAL类型 if (data.length > 3) { return data[3] & 0x1F; } } } // 如果找不到起始码或数据不足,则尝试直接获取 if (data.length > 0) { return data[0] & 0x1F; } return 0; } // 当新帧可用时调用 void _onFrameAvailable(int textureId) { if (!mounted) return; _log("收到帧回调 - 渲染帧 ${_renderedFrameCount + 1}"); // 更新帧时间用于FPS计算 _lastFrameTime = DateTime.now(); // 立即更新UI以显示新帧 setState(() { _renderedFrameCount++; }); } Future _initializeDecoder() async { if (_isInitialized) { await _releaseDecoder(); } _log("正在初始化解码器"); try { final config = VideoDecoderConfig(width: 640, height: 480); final textureId = await VideoDecodePlugin.initDecoder(config); if (textureId != null) { _textureId = textureId; setState(() { _isInitialized = true; _error = ""; _statusText = "就绪"; _renderedFrameCount = 0; }); _log("解码器初始化成功,纹理ID: $_textureId"); } else { setState(() { _error = "获取纹理ID失败"; _statusText = "初始化失败"; }); _log("解码器初始化失败 - 返回空纹理ID"); } } catch (e) { setState(() { _error = e.toString(); _statusText = "初始化错误"; }); _log("解码器初始化错误: $e"); } } // 解码帧(已适配sendFrame,splitNalFromIFrame=false) Future _decodeNextFrame(H264Frame frame, int frameSeq) async { if (_textureId == null || !_isInitialized || !_isPlaying) { return; } try { final timestamp = DateTime.now().microsecondsSinceEpoch; // 如果是I帧,先发送SPS和PPS int nalType = _getNalType(frame.data); if (nalType == NalUnitType.CODED_SLICE_IDR) { // 查找最近的SPS和PPS H264Frame? sps, pps; for (int i = frameSeq - 1; i >= 0; i--) { int t = _getNalType(_h264Frames[i].data); if (sps == null && t == NalUnitType.SPS) sps = _h264Frames[i]; if (pps == null && t == NalUnitType.PPS) pps = _h264Frames[i]; if (sps != null && pps != null) break; } if (sps != null) { await VideoDecodePlugin.sendFrame( frameData: sps.data, frameType: 0, timestamp: timestamp, frameSeq: frameSeq - 2, splitNalFromIFrame: false, ); } if (pps != null) { await VideoDecodePlugin.sendFrame( frameData: pps.data, frameType: 0, timestamp: timestamp, frameSeq: frameSeq - 1, splitNalFromIFrame: false, ); } } await VideoDecodePlugin.sendFrame( frameData: frame.data, frameType: frame.frameType, timestamp: timestamp, frameSeq: frameSeq, splitNalFromIFrame: false, ); } catch (e) { _log("解码帧错误: $e"); } } Future _releaseDecoder() async { if (_textureId != null) { _log("正在释放解码器资源"); try { await VideoDecodePlugin.releaseDecoder(); setState(() { _textureId = null; _isInitialized = false; _statusText = "已释放"; }); _log("解码器资源释放成功"); } catch (e) { _log("释放解码器错误: $e"); } } } Future _startPlaying() async { if (!_isInitialized || _isPlaying || _h264Frames.isEmpty) { _log( "播放条件未满足: 初始化=${_isInitialized}, 播放中=${_isPlaying}, 帧数量=${_h264Frames.length}"); return; } _log("开始播放H264视频"); try { // 重置帧率跟踪 _renderedFrameCount = 0; _lastFrameTime = null; _fps = 0; _currentFrameIndex = 0; // 确保首先发送SPS和PPS await _sendSpsAndPps(); // 开始解码帧 _startDecodingFrames(); setState(() { _isPlaying = true; _statusText = "播放中"; }); _log("播放已开始"); } catch (e) { _log("播放开始错误: $e"); } } // 发送SPS和PPS Future _sendSpsAndPps() async { for (int i = 0; i < math.min(10, _h264Frames.length); i++) { H264Frame frame = _h264Frames[i]; // 检查是否是SPS或PPS (通过检查NAL类型) if (frame.data.length > 4) { int skipBytes = (frame.data[0] == 0x00 && frame.data[1] == 0x00 && frame.data[2] == 0x00 && frame.data[3] == 0x01) ? 4 : 3; if (skipBytes < frame.data.length) { int nalType = frame.data[skipBytes] & 0x1F; if (nalType == NalUnitType.SPS || nalType == NalUnitType.PPS) { _log("发送${nalType == NalUnitType.SPS ? 'SPS' : 'PPS'}数据"); await _decodeNextFrame(frame, i); // 发送后等待一小段时间,确保解码器处理 await Future.delayed(Duration(milliseconds: 30)); } } } } } void _stopPlaying() { if (!_isPlaying) return; _log("停止播放"); _frameTimer?.cancel(); _frameTimer = null; setState(() { _isPlaying = false; _statusText = "已停止"; }); _log("播放已停止"); } void _startDecodingFrames() { _log("开始解码视频帧"); // 使用与原视频接近的帧率 const int frameIntervalMs = 42; // 约23.8 fps (接近原视频23.9fps) _frameTimer = Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) async { if (_currentFrameIndex >= _h264Frames.length) { _log("所有帧已解码,重新开始"); _currentFrameIndex = 0; } final frame = _h264Frames[_currentFrameIndex]; // 未初始化成功时不发送解码帧 if (!_isInitialized || _textureId == null) return; await _decodeNextFrame(frame, _currentFrameIndex); _currentFrameIndex++; }); } void _log(String message) { final timestamp = DateTime.now().toString().split('.').first; final logMessage = "[$timestamp] $message"; setState(() { _logs.add(logMessage); if (_logs.length > 100) { _logs.removeAt(0); } }); // 滚动到底部 WidgetsBinding.instance.addPostFrameCallback((_) { if (_logScrollController.hasClients) { _logScrollController.animateTo( _logScrollController.position.maxScrollExtent, duration: Duration(milliseconds: 200), curve: Curves.easeOut, ); } }); } Widget _buildVideoDisplay() { if (_textureId == null) { return Container( width: 640, height: 480, color: Colors.black54, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.videocam_off, size: 48, color: Colors.white70), SizedBox(height: 16), Text( '无可用纹理', style: TextStyle(color: Colors.white, fontSize: 16), ), ], ), ), ); } return Stack( fit: StackFit.expand, children: [ // 背景色 Container(color: Colors.black), // 无帧时显示加载指示 if (_renderedFrameCount == 0) Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.white70), ), SizedBox(height: 16), Text( '初始化中...', style: TextStyle(color: Colors.white70, fontSize: 14), ), ], ), ), // 视频纹理 - 使用RepaintBoundary和ValueKey确保正确更新 RepaintBoundary( child: Texture( textureId: _textureId!, filterQuality: FilterQuality.medium, key: ValueKey('texture_${_renderedFrameCount}'), ), ), // 丢包效果指示器 - 当有帧被丢弃时,上方显示红色条带 if (_enablePacketLoss && _droppedFramesCount > 0) Positioned( top: 0, left: 0, right: 0, height: 5, child: Container( color: Colors.red.withOpacity(0.8), ), ), // // 显示帧计数 - 调试用 // Positioned( // right: 10, // top: 10, // child: Container( // padding: EdgeInsets.all(5), // decoration: BoxDecoration( // color: Colors.black.withOpacity(0.5), // borderRadius: BorderRadius.circular(4), // ), // constraints: BoxConstraints( // maxWidth: 150, // 限制最大宽度 // ), // child: Column( // crossAxisAlignment: CrossAxisAlignment.end, // mainAxisSize: MainAxisSize.min, // 确保column只占用所需空间 // children: [ // Text( // '帧: $_renderedFrameCount', // style: TextStyle(color: Colors.white, fontSize: 12), // ), // if (_enablePacketLoss) // Text( // '丢帧: $_droppedFramesCount', // style: TextStyle( // color: _droppedFramesCount > 0 // ? Colors.orange // : Colors.white70, // fontSize: 12, // ), // ), // ], // ), // ), // ), ], ); } @override Widget build(BuildContext context) { // 更新FPS计算 if (_lastFrameTime != null && _renderedFrameCount > 0) { final now = DateTime.now(); final elapsed = now.difference(_lastFrameTime!).inMilliseconds; if (elapsed > 0) { _fps = 1000 / elapsed; } } return Scaffold( appBar: AppBar( title: Text('视频解码插件演示'), backgroundColor: Theme.of(context).colorScheme.primaryContainer, ), body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 视频显示区域 - 使用AspectRatio控制大小 AspectRatio( aspectRatio: 16 / 9, child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), child: _buildVideoDisplay(), ), ), // 控制面板和状态信息 - 使用Expanded和SingleChildScrollView Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 状态信息区 - 使用Card使其更紧凑 Card( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 状态行 Text('状态: $_statusText', style: TextStyle(fontWeight: FontWeight.bold)), // FPS和帧数信息 // Padding( // padding: const EdgeInsets.only(top: 4.0), // child: Text( // '解码器FPS: ${_decoderFps.toStringAsFixed(1)}', // style: TextStyle(color: Colors.green), // ), // ), // // Padding( // padding: const EdgeInsets.only(top: 4.0), // child: Text('已渲染帧数: $_renderedFrameCount'), // ), // // // 丢弃帧信息 // Padding( // padding: const EdgeInsets.only(top: 4.0), // child: Text( // '已丢弃帧数: $_droppedFramesCount', // style: TextStyle( // color: _droppedFramesCount > 0 // ? Colors.orange // : Colors.black), // ), // ), // // Padding( // padding: const EdgeInsets.only(top: 4.0), // child: Text('当前帧索引: $_currentFrameIndex'), // ), // // // 参数集状态 // Padding( // padding: const EdgeInsets.only(top: 4.0), // child: Column( // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // Text('参数集状态:'), // Padding( // padding: const EdgeInsets.only( // left: 8.0, top: 2.0), // child: Text( // 'SPS: ${_hasSentSPS ? "已发送" : "未发送"}'), // ), // Padding( // padding: const EdgeInsets.only( // left: 8.0, top: 2.0), // child: Text( // 'PPS: ${_hasSentPPS ? "已发送" : "未发送"}'), // ), // Padding( // padding: const EdgeInsets.only( // left: 8.0, top: 2.0), // child: Text( // 'IDR: ${_hasSentIDR ? "已发送" : "未发送"}'), // ), // ], // ), // ), // // Padding( // padding: const EdgeInsets.only(top: 4.0), // child: Text('总帧数: $_totalFrames'), // ), // // // 错误信息 // if (_error.isNotEmpty) // Padding( // padding: const EdgeInsets.only(top: 4.0), // child: Text( // '错误: $_error', // style: TextStyle( // color: Colors.red, // fontWeight: FontWeight.bold), // ), // ), // H264文件信息 Padding( padding: const EdgeInsets.only(top: 4.0), child: Text('解析的帧数: ${_h264Frames.length}'), ), Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( 'H264文件大小: ${((_h264FileData?.length ?? 0) ~/ 1024)} KB'), ), ], ), ), ), // 控制按钮区 Card( child: Padding( padding: const EdgeInsets.all(8.0), child: Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton( onPressed: _isInitialized ? null : _initializeDecoder, child: Text('初始化'), ), ElevatedButton( onPressed: (!_isInitialized || _isPlaying || _h264Frames.isEmpty) ? null : _startPlaying, child: Text('播放'), ), ElevatedButton( onPressed: (!_isPlaying) ? null : _stopPlaying, child: Text('停止'), ), ElevatedButton( onPressed: (_textureId == null) ? null : _releaseDecoder, child: Text('释放'), ), ElevatedButton( onPressed: () { setState(() { // 强制重绘 _renderedFrameCount++; }); }, child: Text('刷新'), ), ], ), ), ), // 丢包模拟控制面板 Card( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('丢包模拟', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16)), Switch( value: _enablePacketLoss, onChanged: (value) { setState(() { _enablePacketLoss = value; if (value) { _droppedFramesCount = 0; // 重置丢帧计数 } }); }, ), ], ), // 丢包率控制 Row( children: [ Text( '丢包率: ${(_packetLossRate * 100).toStringAsFixed(0)}%'), Expanded( child: Slider( value: _packetLossRate, min: 0.0, max: 1.0, divisions: 20, onChanged: _enablePacketLoss ? (value) { setState(() { _packetLossRate = value; }); } : null, ), ), ], ), // 丢包模式选择 Row( children: [ Expanded( child: CheckboxListTile( dense: true, title: Text('爆发式丢包'), value: _burstPacketLossMode, onChanged: _enablePacketLoss ? (value) { setState(() { _burstPacketLossMode = value!; }); } : null, ), ), Expanded( child: CheckboxListTile( dense: true, title: Text('丢弃I帧'), value: _dropIFrames, onChanged: _enablePacketLoss ? (value) { setState(() { _dropIFrames = value!; }); } : null, ), ), ], ), Row( children: [ Expanded( child: CheckboxListTile( dense: true, title: Text('丢弃P帧'), value: _dropPFrames, onChanged: _enablePacketLoss ? (value) { setState(() { _dropPFrames = value!; }); } : null, ), ), Expanded( child: CheckboxListTile( dense: true, title: Text('丢弃SPS/PPS'), value: _dropSPSPPS, onChanged: _enablePacketLoss ? (value) { setState(() { _dropSPSPPS = value!; }); } : null, ), ), ], ), // 丢包统计信息 Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( '已丢弃帧数: $_droppedFramesCount', style: TextStyle( color: _droppedFramesCount > 0 ? Colors.red : Colors.black), ), ), ], ), ), ), // 日志区域 SizedBox(height: 8), Text('日志:', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 4), Container( height: 280, decoration: BoxDecoration( color: Colors.black, border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(4.0), ), padding: EdgeInsets.all(4), child: ListView.builder( controller: _logScrollController, itemCount: _logs.length, itemBuilder: (context, index) { return Text( _logs[index], style: TextStyle( color: _logs[index].contains('错误') ? Colors.red : Colors.green, fontSize: 11, fontFamily: 'monospace', ), ); }, ), ), ], ), ), ), ), ], ), ), ); } } // 添加错误帧绘制器 class ErrorFramePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final random = math.Random(); final paint = Paint() ..color = Colors.red.withOpacity(0.2) ..style = PaintingStyle.fill; // 绘制随机条纹效果 for (int i = 0; i < 20; i++) { double y = random.nextDouble() * size.height; canvas.drawRect( Rect.fromLTWH(0, y, size.width, 3 + random.nextDouble() * 5), paint, ); } // 绘制马赛克块 for (int i = 0; i < 50; i++) { double x = random.nextDouble() * size.width; double y = random.nextDouble() * size.height; double blockSize = 10 + random.nextDouble() * 30; canvas.drawRect( Rect.fromLTWH(x, y, blockSize, blockSize), Paint() ..color = Color.fromRGBO(random.nextInt(255), random.nextInt(255), random.nextInt(255), 0.3)); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return true; // 每次都重新绘制以产生动态效果 } }