1

我已经使用 Flutter 的 CustomPainter 类来创建使用路径的生成静止图像(参见下面的代码)。我希望能够无限期地为这些图像制作动画。最直接的方法是什么?

import 'package:flutter/material.dart';

void main() => runApp(
      MaterialApp(
        home: PathExample(),
      ),
    );

class PathExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: PathPainter(),
    );
  }
}

class PathPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.grey[200]
      ..style = PaintingStyle.fill
      ..strokeWidth = 0.0;

    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);

    Path path2 = Path();
    for (double i = 0; i < 200; i++) {
      Random r = new Random();
      path2.moveTo(sin(i / 2.14) * 45 + 200, i * 12);
      path2.lineTo(sin(i / 2.14) * 50 + 100, i * 10);
      paint.style = PaintingStyle.stroke;
      paint.color = Colors.red;
      canvas.drawPath(path2, paint);
    }

    Path path = Path();
    paint.color = Colors.blue;
    paint.style = PaintingStyle.stroke;
    for (double i = 0; i < 30; i++) {
      path.moveTo(100, 50);
      // xC, yC, xC, yC, xEnd, yEnd
      path.cubicTo(
          -220, 300, 500, 600 - i * 20, size.width / 2 + 50, size.height - 50);
      canvas.drawPath(path, paint);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
4

1 回答 1

2

要做到这一点,您需要做很多事情TickerProviderStateMixin——本质上,您需要创建和管理自己的Ticker.

我在下面的一个简单的构建器小部件中完成了这项工作。它只是在每次出现滴答声时安排构建,然后在该票证期间使用给定值构建。为方便起见,我添加了一个totalElapsed参数和一个sinceLastDraw参数,但您可以根据您正在做的最方便的方式轻松选择一个或另一个。

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

void main() => runApp(
      MaterialApp(
        home: PathExample(),
      ),
    );

class PathExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TickerBuilder(builder: (context, sinceLast, total) {
      return CustomPaint(
        painter: PathPainter(total.inMilliseconds / 1000.0),
      );
    });
  }
}

class TickerBuilder extends StatefulWidget {
  
  // this builder function is used to create the widget which does
  // whatever it needs to based on the time which has elapsed or the
  // time since the last build. The former is useful for position-based
  // animations while the latter could be used for velocity-based
  // animations (i.e. oldPosition + (time * velocity) = newPosition).
  final Widget Function(BuildContext context, Duration sinceLastDraw, Duration totalElapsed) builder;

  const TickerBuilder({Key? key, required this.builder}) : super(key: key);
  
  @override
  _TickerBuilderState createState() => _TickerBuilderState();
}  

class _TickerBuilderState extends State<TickerBuilder> {
  
  // creates a ticker which ensures that the onTick function is called every frame
  late final Ticker _ticker = Ticker(onTick);
  
  // the total is the time that has elapsed since the widget was created.
  // It is initially set to zero as no time has elasped when it is first created.
  Duration total = Duration.zero;
  // this last draw time is saved during each draw cycle; this is so that
  // a time between draws can be calculated
  Duration lastDraw = Duration.zero;
  
  void onTick(Duration elapsed) {
    // by calling setState every time this function is called, we're
    // triggering this widget to be rebuilt on every frame.
    // This is where the indefinite animation part comes in!
    setState(() {
      total = elapsed;
    });
  }
  
  @override
  void initState() {
    super.initState();
    _ticker.start();
  }
    
    @override
  void didChangeDependencies() {
    _ticker.muted = !TickerMode.of(context);
    super.didChangeDependencies();
  }
  
  @override
  Widget build(BuildContext context) {
    final result = widget.builder(context, total - lastDraw , total);
    lastDraw = total;
    return result;
  }
  
  @override
  void dispose() {
    _ticker.stop();
    super.dispose();
  }
}
  
class PathPainter extends CustomPainter {
  final double pos;

  PathPainter(this.pos);

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.grey
      ..style = PaintingStyle.fill
      ..strokeWidth = 0.0;

    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);

    Path path2 = Path();
    for (double i = 0; i < 200; i++) {
      Random r = new Random();
      path2.moveTo(sin(i / 2.14 + pos) * 45 + 200, (i * 12));
      path2.lineTo(sin(i / 2.14 + pos) * 50 + 100, (i * 10));
      paint.style = PaintingStyle.stroke;
      paint.color = Colors.red;
      canvas.drawPath(path2, paint);
    }

    Path path = Path();
    paint.color = Colors.blue;
    paint.style = PaintingStyle.stroke;
    for (double i = 0; i < 30; i++) {
      path.moveTo(100, 50);
      // xC, yC, xC, yC, xEnd, yEnd
      path.cubicTo(
        -220,
        300,
        500,
        600 - i * 20,
        size.width / 2 + 50,
        size.height - 50,
      );
      canvas.drawPath(path, paint);
    }
  }

  // in this particular case, this is rather redundant as 
  // the animation is happening every single frame. However,
  // in the case where it doesn't need to animate every frame, you
  // should implement it such that it only returns true if it actually
  // needs to redraw, as that way the flutter engine can optimize
  // its drawing and use less processing power & battery.
  @override
  bool shouldRepaint(PathPainter old) => old.pos != pos;
}

需要注意的几件事 - 首先,在这种情况下,绘图非常次优。与其每帧都重新绘制背景,不如使用 Container 或 DecoratedBox 将其制成静态背景。其次,绘制对象在每一帧都被重新创建和使用——如果它们是恒定的,它们可以被实例化一次并一遍又一遍地重复使用。

此外,由于 WidgetBuilder 将运行很多,您将需要确保在其构建功能中尽可能少地执行 - 您不想在那里构建整个小部件树,而是将它移到树中尽可能低的位置,以便它只构建实际动画的东西(就像我在本例中所做的那样)。

于 2021-04-08T06:24:15.003 回答