Skip to content

Instantly share code, notes, and snippets.

@roipeker
Last active June 9, 2025 23:21
Show Gist options
  • Save roipeker/80b2f63e3024edc70bb410d9a157f347 to your computer and use it in GitHub Desktop.
Save roipeker/80b2f63e3024edc70bb410d9a157f347 to your computer and use it in GitHub Desktop.
An "animated builder" CustomPainter with basic stateless control. For quick drawing animations.
// Example:
// ```dart
// return AnimatedCanvasBuilder(
// duration: const Duration(seconds: 2),
// delay: const Duration(seconds: 1),
// curve: Curves.easeInOut,
// reverse: true,
// repeat: true,
// drawForeground: (Canvas canvas, Size size, double ratio) {
// final paint = Paint()
// ..color = Colors.blue
// ..strokeWidth = 4
// ..style = PaintingStyle.stroke;
// final path = Path()
// ..addRect(
// Rect.fromLTWH(10, 10, size.width - 20, 200 - 20),
// );
// final metrics = path.computeMetrics().first;
// final partialPath = metrics.extractPath(0, metrics.length * ratio);
// canvas.drawPath(partialPath, paint);
// },
// child: Text('Animation'),
// ...
//```
// Example:
// ```dart
// return AnimatedCanvasBuilder(
// duration: const Duration(seconds: 2),
// delay: const Duration(seconds: 1),
// curve: Curves.easeInOut,
// reverse: true,
// repeat: true,
// drawForeground: (Canvas canvas, Size size, double ratio) {
// final paint = Paint()
// ..color = Colors.blue
// ..strokeWidth = 4
// ..style = PaintingStyle.stroke;
// final path = Path()
// ..addRect(
// Rect.fromLTWH(10, 10, size.width - 20, 200 - 20),
// );
// final metrics = path.computeMetrics().first;
// final partialPath = metrics.extractPath(0, metrics.length * ratio);
// canvas.drawPath(partialPath, paint);
// },
// child: Text('Animation'),
// ...
//```
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
typedef CanvasDrawCallback = void Function(
Canvas canvas, Size size, double ratio);
enum AnimatedCanvasMode {
internal,
listenable,
}
class AnimatedCanvasBuilder extends StatefulWidget {
/// INTERNAL animation mode
final Duration? duration;
final Curve curve;
final Duration delay;
final bool reverse;
final bool repeat;
final int? loopCount; // null = infinite
final double startAt;
final VoidCallback? onEnd;
/// LISTENABLE animation mode
final Listenable? listenable;
final double Function()? getValue;
/// Shared
final CanvasDrawCallback? drawBackground;
final CanvasDrawCallback? drawForeground;
final Widget? child;
final Size size;
final bool isComplex;
const AnimatedCanvasBuilder({
super.key,
required this.duration,
this.curve = Curves.linear,
this.delay = Duration.zero,
this.child,
this.drawBackground,
this.drawForeground,
this.onEnd,
this.reverse = false,
this.repeat = false,
this.loopCount,
this.startAt = 0.0,
this.size = Size.zero,
this.isComplex = false,
}) : listenable = null,
getValue = null;
const AnimatedCanvasBuilder.listenable({
super.key,
required this.listenable,
required this.getValue,
this.child,
this.drawBackground,
this.drawForeground,
this.size = Size.zero,
this.isComplex = false,
}) : duration = null,
curve = Curves.linear,
delay = Duration.zero,
reverse = false,
repeat = false,
loopCount = null,
startAt = 0.0,
onEnd = null;
@override
State<AnimatedCanvasBuilder> createState() => _AnimatedCanvasBuilderState();
}
class _AnimatedCanvasBuilderState extends State<AnimatedCanvasBuilder>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
Future<void> _initAndStart() async {
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
_animation = CurvedAnimation(
parent: _controller,
curve: widget.curve,
);
if (widget.reverse && !widget.repeat) {
_controller.value = 1.0;
}
if (widget.delay.inMilliseconds > 0 &&
!(widget.reverse && !widget.repeat)) {
await Future.delayed(widget.delay);
}
_controller.value = widget.startAt;
if (widget.repeat && widget.loopCount != null) {
int cycles = 0;
bool forward = true;
void handleStatus(AnimationStatus status) {
if (status == AnimationStatus.completed ||
status == AnimationStatus.dismissed) {
cycles++;
if (cycles >= widget.loopCount!) {
_controller.removeStatusListener(handleStatus);
widget.onEnd?.call();
} else {
forward ? _controller.reverse() : _controller.forward();
forward = !forward;
}
}
}
_controller.addStatusListener(handleStatus);
forward ? _controller.forward() : _controller.reverse();
} else if (widget.repeat) {
_controller.repeat(reverse: widget.reverse);
} else if (widget.reverse) {
await _controller.reverse(from: 1.0);
widget.onEnd?.call();
} else {
await _controller.forward(from: widget.startAt);
widget.onEnd?.call();
}
}
@override
void initState() {
super.initState();
if (widget.listenable == null) {
_initAndStart();
}
}
@override
void didUpdateWidget(covariant AnimatedCanvasBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.listenable == null) {
if (widget.duration != oldWidget.duration) {
_controller.duration = widget.duration!;
}
if (widget.curve != oldWidget.curve) {
_animation = CurvedAnimation(
parent: _controller,
curve: widget.curve,
);
}
}
}
@override
void dispose() {
if (widget.listenable == null) {
_controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final animation = widget.listenable ?? _animation;
final getValue = widget.getValue ?? () => _animation.value;
return RepaintBoundary(
child: AnimatedBuilder(
animation: animation,
builder: (_, child) {
return CustomPaint(
size: widget.size,
isComplex: widget.isComplex,
willChange: true,
painter: widget.drawBackground != null
? _InlinePainter(
ratio: getValue(),
drawCallback: widget.drawBackground!,
)
: null,
foregroundPainter: widget.drawForeground != null
? _InlinePainter(
ratio: getValue(),
drawCallback: widget.drawForeground!,
)
: null,
child: child,
);
},
child: widget.child,
),
);
}
}
class _InlinePainter extends CustomPainter {
final double ratio;
final CanvasDrawCallback drawCallback;
_InlinePainter({
required this.ratio,
required this.drawCallback,
});
@override
void paint(Canvas canvas, Size size) {
drawCallback(canvas, size, ratio);
}
@override
bool shouldRepaint(covariant _InlinePainter oldDelegate) =>
oldDelegate.ratio != ratio;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment