You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 20:02:31