Flutter - 资产中的图像在动画过程中消失

问题描述 投票:0回答:1

我想在我的应用程序中制作动画,在其中加载图像并使其沿着特定路径移动。我画了一条由几个点组成的线,首先我尝试让一个点沿着移动 - 一切正常。

这是我的代码:

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;

  @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;
        });
      });

  }

  @override
  void dispose() {
    myController.dispose();
    super.dispose();
  }

  Future<ui.Image> _loadImage(String imagePath) async {
    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;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text("Turtle Maths")),
        body: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            return Column(
              children: [
                AnimatedBuilder(
                  animation: controller,
                  builder: (BuildContext context, _) {
                    return TextField(
                      controller: myController,
                      keyboardType: TextInputType.number,
                      decoration: InputDecoration(
                          prefixIcon: const Icon(PiSymbol.pi_outline),
                          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: () {
                                    setState(() {
                                      if (myController.text.isNotEmpty) {
                                        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\u{00B3} + ${myController.text}x\u{00B2} + ${myController.text}x + ${myController.text} = 0",
                      style: const TextStyle(fontSize: 18)),
                ),
                Expanded(          
                  child: FutureBuilder<ui.Image>(
                    future: _loadImage("assets/images/turtle.png"),
                    builder:(BuildContext context, AsyncSnapshot<ui.Image> snapshot){
                      if (snapshot.connectionState == ConnectionState.done){
                        return CustomPaint(
                        painter: MyPainter(constraints.maxWidth / 2,
                            constraints.maxHeight / 2, ratio, _progress, snapshot.data),
                        size: Size(constraints.maxWidth, constraints.maxHeight));
                      }
                      else{
                        return CustomPaint(
                        painter: MyPainter(constraints.maxWidth / 2,
                            constraints.maxHeight / 2, ratio, _progress),
                        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) async {
    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 (image == null) {
      canvas.drawCircle(pos!.position, linePaint.strokeWidth / 2, linePaint);
    } else {
      canvas.drawImage(image!, pos!.position, linePaint);
    }
  }

  Path getPath() {
    return Path()
      ..moveTo(_width, _height)
      ..lineTo(_width + _ratio, _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;
  }
}

我之前有过空错误,所以我做了一个 if 语句来检查图像是否存在,现在图像在最开始加载,然后我在动画期间看到点,图像仅在最后再次出现。

有人可以解释一下我在这里做错了什么以及正确的方法是什么?预先感谢。

flutter dart flutter-animation
1个回答
0
投票

这里的问题是未来建造者的未来是每一帧都被建造的。

future: _loadImage("assets/images/turtle.png")

这里,图像正在每一帧上加载。

来自官方文档https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

管理未来 未来一定是早先获得的,例如在 State.initState、State.didUpdateWidget 或 State.didChangeDependency 期间。在构造 FutureBuilder 时,不得在 State.build 或 StatelessWidget.build 方法调用期间创建它。如果 future 与 FutureBuilder 同时创建,那么每次重建 FutureBuilder 的父级时,异步任务都会重新启动。

一般准则是假设每个构建方法都可以在每帧被调用,并将省略的调用视为优化。

因此,一旦将其替换为在初始化状态下创建的某个 future,您就会显示图像。

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 Future<ui.Image> _imageFuture; // added this

  @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;
        });
      });

    _imageFuture = _loadImage("assets/images/turtle.png"); // added this
  }

  @override
  void dispose() {
    myController.dispose();
    super.dispose();
  }

  Future<ui.Image> _loadImage(String imagePath) async {
    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;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text("Turtle Maths")),
        body: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            return Column(
              children: [
                AnimatedBuilder(
                  animation: controller,
                  builder: (BuildContext context, _) {
                    return TextField(
                      controller: myController,
                      keyboardType: TextInputType.number,
                      decoration: InputDecoration(
                          prefixIcon: const Icon(Icons.pie_chart),
                          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: () {
                                    setState(() {
                                      if (myController.text.isNotEmpty) {
                                        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\u{00B3} + ${myController.text}x\u{00B2} + ${myController.text}x + ${myController.text} = 0",
                      style: const TextStyle(fontSize: 18)),
                ),
                Expanded(
                    child: FutureBuilder<ui.Image>(
                        future: _imageFuture,   //replaced this line
                        builder: (BuildContext context,
                            AsyncSnapshot<ui.Image> snapshot) {
                          if (snapshot.connectionState ==
                              ConnectionState.done) {
                            return CustomPaint(
                                painter: MyPainter(
                                    constraints.maxWidth / 2,
                                    constraints.maxHeight / 2,
                                    ratio,
                                    _progress,
                                    snapshot.data),
                                willChange: true,
                                size: Size(constraints.maxWidth,
                                    constraints.maxHeight));
                          } else {
                            return CustomPaint(
                                painter: MyPainter(
                                    constraints.maxWidth / 2,
                                    constraints.maxHeight / 2,
                                    ratio,
                                    _progress),
                                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) async {
    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 (image == null) {
      canvas.drawCircle(pos!.position, linePaint.strokeWidth / 2, linePaint);
    } else {
      canvas.drawImage(image!, pos!.position, linePaint);
    }
  }

  Path getPath() {
    return Path()
      ..moveTo(_width, _height)
      ..lineTo(_width + _ratio, _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;
  }
}

© www.soinside.com 2019 - 2024. All rights reserved.