我已经使用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;
}
要做到这一点,您需要做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将大量运行,您将需要确保在其构建功能中尽可能少地执行操作——您不希望在那里构建整个小部件树,而是将其在树中移动到尽可能低的位置,以便它只构建实际动画化的东西(就像我在本例中所做的那样(。