【问题标题】:Flutter: how to create generative animations using CustomPainterFlutter:如何使用 CustomPainter 创建生成动画
【发布时间】:2021-04-07 20:43:39
【问题描述】:

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

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

【问题讨论】:

    标签: flutter dart generative-art


    【解决方案1】:

    要做到这一点,您需要做很多 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 将运行很多,因此您需要确保在其构建功能中尽可能少地执行 - 您不会想要在那里建立一个完整的小部件树,而是将它移动到树中尽可能低的位置,以便它只构建实际动画的东西(就像我在本例中所做的那样)。

    【讨论】:

    • @mtmmckenzie 谢谢你;然而,当我说“无限期”时,我的意思是没有任何重复。您提供的代码具有确定的持续时间并重复(即基本上是振荡的)。我正在寻找的是在 Processing 或 OpenFrameworks 中的 draw() 等价物。
    • 哎呀,错过了'无限期'哈哈。
    • @RonH 已编辑 - 应该这样做 =)
    • 我可能让它比绝对需要的稍微复杂一些。但是你不想只做一个循环的原因是你不能保证何时/多久绘制一次,而不是处理。在支持 120hz 的设备上,绘图的发生频率可能​​是支持 60hz 的设备的两倍,因此动画速度不会相同。我也会对答案添加更多解释。
    • 感谢您添加 cmets - 他们帮助我澄清了事情。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-20
    • 2021-11-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多