Flutter:资源图片在动画过程中消失的问题排查
Flutter路径动画图片消失问题分析与修复
问题根源
- 图片重复加载:每次动画更新触发
setState时,FutureBuilder会重新执行_loadImage,导致动画过程中图片处于加载状态,临时显示圆点。 - paint方法误用异步:
CustomPainter.paint是同步方法,标记为async会打乱Flutter绘制流程,无法正常渲染图片。 - 图片位置未居中:直接用路径点作为图片左上角坐标,视觉上图片没有沿路径中心移动。
修复步骤
1. 提前加载并缓存图片
在initState中完成图片加载,避免重复请求:
late ui.Image _turtleImage; bool _isImageLoaded = false; @override void initState() { super.initState(); controller = AnimationController( duration: const Duration(milliseconds: 3000), vsync: this); animation = Tween(begin: 0.0, end: 1.0).animate(controller) ..addListener(() { setState(() { _progress = animation.value; }); }); // 提前加载图片并更新状态 _loadImage("assets/images/turtle.png").then((image) { setState(() { _turtleImage = image; _isImageLoaded = true; }); }); }
2. 移除FutureBuilder,直接使用缓存图片
替换原Expanded中的FutureBuilder部分,避免重复加载:
Expanded( child: CustomPaint( painter: MyPainter( constraints.maxWidth / 2, constraints.maxHeight / 2, ratio, _progress, _isImageLoaded ? _turtleImage : null, ), size: Size(constraints.maxWidth, constraints.maxHeight), ), )
3. 修正paint方法的异步问题
去掉paint方法的async修饰符,确保绘制流程同步执行,同时调整图片位置让中心对齐路径点:
@override void paint(Canvas canvas, Size size) { Paint linePaint = Paint() ..strokeWidth = 20 ..strokeCap = StrokeCap.butt ..style = PaintingStyle.stroke; var path = getPath(); ui.PathMetrics pathMetrics = path.computeMetrics(); ui.PathMetric pathMetric = pathMetrics.elementAt(0); final pos = pathMetric.getTangentForOffset(pathMetric.length * _progress); Path extracted = pathMetric.extractPath(0.0, pathMetric.length * _progress); linePaint.strokeWidth = 6; canvas.drawPath(extracted, linePaint); if (pos == null) return; if (image == null) { canvas.drawCircle(pos.position, linePaint.strokeWidth / 2, linePaint); } else { // 偏移图片,让中心对齐路径点 final offset = Offset( pos.position.dx - image!.width / 2, pos.position.dy - image!.height / 2, ); canvas.drawImage(image!, offset, Paint()); } }
4. 完善图片加载异常处理
在_loadImage中添加错误捕获,避免加载失败导致崩溃:
Future<ui.Image> _loadImage(String imagePath) async { try { ByteData bd = await rootBundle.load(imagePath); final Uint8List bytes = Uint8List.view(bd.buffer); final ui.Codec codec = await ui.instantiateImageCodec(bytes, targetHeight: 60, targetWidth: 60); final ui.Image image = (await codec.getNextFrame()).image; return image; } catch (e) { throw Exception("Failed to load image: $e"); } }
完整修复代码
import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Turtle Maths', theme: ThemeData( primarySwatch: Colors.green, visualDensity: VisualDensity.adaptivePlatformDensity, useMaterial3: false, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { final myController = TextEditingController(); int ratio = 10; double _progress = 0.0; late Animation<double> animation; late AnimationController controller; late ui.Image _turtleImage; bool _isImageLoaded = false; @override void initState() { super.initState(); controller = AnimationController( duration: const Duration(milliseconds: 3000), vsync: this); animation = Tween(begin: 0.0, end: 1.0).animate(controller) ..addListener(() { setState(() { _progress = animation.value; }); }); _loadImage("assets/images/turtle.png").then((image) { setState(() { _turtleImage = image; _isImageLoaded = true; }); }); } @override void dispose() { myController.dispose(); controller.dispose(); super.dispose(); } Future<ui.Image> _loadImage(String imagePath) async { try { ByteData bd = await rootBundle.load(imagePath); final Uint8List bytes = Uint8List.view(bd.buffer); final ui.Codec codec = await ui.instantiateImageCodec(bytes, targetHeight: 60, targetWidth: 60); final ui.Image image = (await codec.getNextFrame()).image; return image; } catch (e) { throw Exception("Failed to load image: $e"); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Turtle Maths")), body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Column( children: [ TextField( controller: myController, keyboardType: TextInputType.number, decoration: InputDecoration( prefixIcon: const Icon(Icons.pi_outlined), hintText: "Coefficient", border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide( color: Colors.black, width: 1, style: BorderStyle.solid, ), ), suffixIcon: Container( margin: const EdgeInsets.all(8), child: ElevatedButton( style: ElevatedButton.styleFrom( minimumSize: const Size(100, 50), iconColor: Colors.red, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30.0), ), ), onPressed: () { if (myController.text.isNotEmpty) { setState(() { ratio = int.parse(myController.text); }); controller.reset(); controller.forward(); } }, child: const Text("RUN")))), ), Padding( padding: const EdgeInsets.all(8.0), child: Text( "${myController.text}x³ + ${myController.text}x² + ${myController.text}x + ${myController.text} = 0", style: const TextStyle(fontSize: 18)), ), Expanded( child: CustomPaint( painter: MyPainter( constraints.maxWidth / 2, constraints.maxHeight / 2, ratio, _progress, _isImageLoaded ? _turtleImage : null, ), size: Size(constraints.maxWidth, constraints.maxHeight), ), ), ], ); }, )); } } class MyPainter extends CustomPainter { ui.Image? image; double _width = 0; double _height = 0; int _ratio = 0; final double _progress; MyPainter(width, height, ratio, this._progress, [this.image]) { _width = width; _height = height; _ratio = ratio * 10; } @override void paint(Canvas canvas, Size size) { Paint linePaint = Paint() ..strokeWidth = 20 ..strokeCap = StrokeCap.butt ..style = PaintingStyle.stroke; var path = getPath(); ui.PathMetrics pathMetrics = path.computeMetrics(); ui.PathMetric pathMetric = pathMetrics.elementAt(0); final pos = pathMetric.getTangentForOffset(pathMetric.length * _progress); Path extracted = pathMetric.extractPath(0.0, pathMetric.length * _progress); linePaint.strokeWidth = 6; canvas.drawPath(extracted, linePaint); if (pos == null) return; if (image == null) { canvas.drawCircle(pos.position, linePaint.strokeWidth / 2, linePaint); } else { final offset = Offset( pos.position.dx - image!.width / 2, pos.position.dy - image!.height / 2, ); canvas.drawImage(image!, offset, Paint()); } } Path getPath() { return Path() ..moveTo(_width, _height) ..lineTo(_width + _ratio, _height) ..lineTo(_width + _ratio, _height - _ratio) ..lineTo(_width - _ratio, _height - _ratio) ..lineTo(_width - _ratio, _height + _ratio); } @override bool shouldRepaint(covariant MyPainter oldDelegate) { return oldDelegate._progress != _progress || oldDelegate.image != image || oldDelegate._ratio != _ratio; } }
内容的提问来源于stack exchange,提问作者Snork Maiden
相关产品推荐
相关产品推荐

