Last active
June 9, 2025 23:21
-
-
Save roipeker/80b2f63e3024edc70bb410d9a157f347 to your computer and use it in GitHub Desktop.
An "animated builder" CustomPainter with basic stateless control. For quick drawing animations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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