Add animation to a widget app
Animations follow the same message loop as the rest of an Artisanal app. An animation schedules a tick, updates its value when that message arrives, and stops scheduling ticks when it is complete. This keeps animation state visible to the normal update and testing pipeline.
Core Concepts
TEA-Native Lifecycle
Animations in Artisanal work by scheduling future messages:
- An
AnimationControlleris started, returning aCmdthat schedules a tick. - The TEA loop delivers an
AnimationTickMsgto the component'shandleUpdatemethod. - The controller processes the tick, updates its value, and returns a new
Cmdto schedule the next frame. - This cycle continues until the animation completes, self-terminating by not returning a new
Cmd.
AnimationController
The AnimationController manages the state of a single value (usually 0.0 to 1.0) over time.
final controller = AnimationController(
duration: const Duration(milliseconds: 300),
fps: 30, // Target frames per second
);
// Start the animation (returns a Cmd)
final cmd = controller.forward();
Key features of AnimationController:
- Directional Playback:
forward(),reverse(),animateTo(),animateBack(). - Indefinite Loops:
repeat()with optionalreverse(ping-pong) mode. - Custom Ranges: Define
lowerBoundandupperBound. - Status Tracking: Listen for status changes (dismissed, forward, reverse, completed).
AnimationMixin
Integrating animations into a StatefulWidget is simplified using AnimationMixin. It automatically handles:
- Correctly dispatching
AnimationTickMsgto the right controller. - Chaining the next frame's
Cmd. - Automatic disposal of controllers when the widget is removed.
class _MyState extends State<MyWidget> with AnimationMixin {
late AnimationController _fade;
void initState() {
super.initState();
_fade = createAnimationController(duration: const Duration(seconds: 1));
_fade.addListener(() => setState(() {}));
}
Cmd? handleInit() => _fade.forward();
Widget build(BuildContext context) {
return Opacity(opacity: _fade.value, child: Text('Hello'));
}
}
AnimatedBuilder
AnimatedBuilder rebuilds a subtree whenever an Animation changes value,
keeping the rebuild scope small. Pass an animation and a builder; the
optional child subtree is built once and handed to the builder so the static
parts are not recreated on every tick.
class FadeLabel extends StatefulWidget {
final String text;
const FadeLabel(this.text, {super.key});
State createState() => _FadeLabelState();
}
class _FadeLabelState extends State<FadeLabel> with AnimationMixin {
late final AnimationController _ctrl = createAnimationController(
duration: const Duration(milliseconds: 800),
)..repeat(reverse: true);
Widget build(BuildContext context) {
// `child` is the static subtree — built once, not per-tick.
return AnimatedBuilder(
animation: _ctrl,
child: Text(widget.text),
builder: (context, child) =>
Opacity(opacity: _ctrl.value, child: child),
);
}
}
ListenableBuilder and ValueListenableBuilder are structurally identical
but accept a Listenable / ValueListenable instead of an Animation,
making them useful for any change-notifier source beyond AnimationController.
ImplicitlyAnimated Widgets
ImplicitlyAnimatedWidget (and its companion AnimatedWidgetBaseState)
provide a higher-level pattern for widgets that animate to a new target
value whenever a property changes, without the caller having to create or
manage a controller.
class AnimatedOpacity extends ImplicitlyAnimatedWidget {
final double opacity;
final Widget child;
const AnimatedOpacity({
required this.opacity,
required this.child,
super.duration = const Duration(milliseconds: 300),
super.curve = Curves.easeInOut,
super.key,
});
AnimatedWidgetBaseState<AnimatedOpacity> createState() =>
_AnimatedOpacityState();
}
class _AnimatedOpacityState
extends AnimatedWidgetBaseState<AnimatedOpacity> {
Tween<double>? _opacity;
void forEachTween(TweenVisitor visitor) {
_opacity = visitor(
_opacity,
widget.opacity,
(value) => Tween<double>(begin: value as double),
) as Tween<double>?;
}
Widget build(BuildContext context) =>
Opacity(opacity: _opacity!.evaluate(animation), child: widget.child);
}
How it works:
- Whenever a property changes,
AnimatedWidgetBaseStatecallsforEachTweento update the tween'sbeginvalue and reset the controller. - The controller runs forward from
begintoendoverduration. - The
buildmethod reads the current interpolated value viatween.evaluate(animation).
This pattern is used by Flutter's built-in AnimatedOpacity, AnimatedAlign,
and AnimatedContainer. Custom implicitly-animated widgets follow the same
shape.
Animation Timeline
For complex, multi-step animations, the AnimationTimeline provides a declarative choreography system.
Steps
A timeline consists of a series of steps:
forward/reverse: Run a controller in the specified direction.animateTo/animateBack: Move a controller to a specific value.delay: Wait for a duration.parallel: Start multiple steps simultaneously and wait for all to complete.callback: Run arbitrary logic as a step.
Orchestration Presets
The timeline includes many high-level presets for common patterns:
staggered: Run multiple controllers with a fixed gap between starts.pulse: Animate forward, hold, then reverse.cascade: Run a pulse on a list of controllers in sequence.wave: A traveling wave effect (forward sweep, then reverse sweep).fan: Staggered entrance, synchronized exit.breath: All controllers animate forward and reverse together.ripple: Center-origin ripple effect.
_timeline = AnimationTimeline(
steps: [
AnimationTimelineStep.forward(_fade),
AnimationTimelineStep.delay(const Duration(milliseconds: 80)),
AnimationTimelineStep.parallel([
AnimationTimelineStep.forward(_slide),
AnimationTimelineStep.forward(_scale),
]),
],
repeat: true,
alternate: true,
);
Curves and Tweens
Curves
Curves control the rate of change over time. Artisanal includes a comprehensive set of standard curves:
linear,decelerate,easeIn,easeOut,easeInOut.elasticIn,elasticOut,bounceIn,bounceOut, etc.- Custom
CubicBezier curves. Intervalfor applying a curve to a sub-range of an animation.
Tweens
Tweens interpolate between two values.
IntTween,DoubleTween,ColorTween,RectTween.- Chain tweens using
.chain(CurveTween(...)).
Visual Effects (UV)
The ultraviolet package provides post-processing effects that can be applied to rendered buffers. These are often driven by animations.
ColorMatrix
A 4x5 RGBA color matrix for transforming cell styles.
- Grayscale:
ColorMatrix.grayscale(). - Invert:
ColorMatrix.invert(). - Tint:
ColorMatrix.tint(color, amount: 0.5). - Gain/Attenuation: Scale RGB brightness.
CRT & Terminal Filters
Composite filters that simulate vintage hardware:
PhosphorFilter: Classic green phosphor monitor.AmberTerminalFilter: Warm monochrome display.CrtFilter: Scanlines, vignette, and slight wave distortion.GhostingFilter: Temporal persistence (afterimage trails).
// Apply a CRT effect with persistence
final filter = CrtTrailFilter(
scanline: 0.1,
persistence: 0.32,
);