layout_motion 0.4.0
layout_motion: ^0.4.0 copied to clipboard
Automatic FLIP layout animations for Flutter. Wrap any Column, Row, Wrap, or Stack to animate add, remove, and reorder with zero configuration.
layout_motion #
Automatic FLIP layout animations for Flutter. Wrap any Column, Row, Wrap, or Stack to animate child additions, removals, and reorders with zero configuration.
Features #
- Zero-config — wrap your layout widget with
MotionLayoutand you're done - FLIP technique — GPU-accelerated
Transformanimations (paint phase only, no relayout per frame) - Add / Remove / Reorder — all detected automatically via key-based diffing
- Staggered animations — cascading delays with configurable direction
- Spring physics — physics-based move animations with named presets
- Transition composition — combine transitions with
+operator:FadeIn() + SlideIn() - Per-child curves — separate curves for move, enter, and exit animations
- Lifecycle callbacks —
onAnimationStart,onAnimationComplete,onChildEnter,onChildExit - Auto reduced motion — respects system accessibility settings by default
- Customizable transitions — built-in presets plus easy custom transitions
- Interruption-safe — mid-animation changes produce smooth redirects
- Accessible — exiting children are excluded from the semantic tree and pointer events
- Zero dependencies — only depends on the Flutter SDK
Getting Started #
Add to your pubspec.yaml:
dependencies:
layout_motion: ^0.4.0
Usage #
Basic #
MotionLayout(
child: Column(
children: [
for (final item in items)
ListTile(key: ValueKey(item.id), title: Text(item.name)),
],
),
)
Important: All children must have unique Keys.
Configurable #
MotionLayout(
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
enterTransition: const SlideIn(offset: Offset(0, 0.15)),
exitTransition: const FadeOut(),
clipBehavior: Clip.hardEdge,
child: Wrap(children: [...]),
)
Staggered Animations #
MotionLayout(
duration: const Duration(milliseconds: 400),
staggerDuration: const Duration(milliseconds: 50),
staggerFrom: StaggerFrom.first, // or .last, .center
enterTransition: const FadeSlideIn(),
exitTransition: const FadeOut(),
child: Column(children: [...]),
)
Spring Physics #
MotionLayout(
spring: MotionSpring.bouncy, // or .gentle, .smooth, .stiff
child: Column(children: [...]),
)
// Custom spring:
MotionLayout(
spring: const MotionSpring(stiffness: 250, damping: 18, mass: 1.2),
child: Column(children: [...]),
)
Transition Composition #
Combine multiple transitions with the + operator:
MotionLayout(
enterTransition: const FadeIn() + const SlideIn() + const ScaleIn(scale: 0.9),
exitTransition: const FadeOut() + const ScaleOut(scale: 0.5),
child: Column(children: [...]),
)
Per-Child Curve Control #
MotionLayout(
moveCurve: Curves.easeOutCubic,
enterCurve: Curves.easeOut,
exitCurve: Curves.easeIn,
child: Column(children: [...]),
)
Lifecycle Callbacks #
MotionLayout(
onAnimationStart: () => print('Animations started'),
onAnimationComplete: () => print('All done'),
onChildEnter: (key) => print('$key entering'),
onChildExit: (key) => print('$key exiting'),
child: Column(children: [...]),
)
Row #
MotionLayout(
enterTransition: const SlideIn(offset: Offset(0.15, 0)),
exitTransition: const FadeOut(),
child: Row(
children: [
for (final tag in tags)
Chip(key: ValueKey(tag), label: Text(tag)),
],
),
)
Stack #
MotionLayout(
child: Stack(
children: [
for (final item in items)
Positioned(
key: ValueKey(item.id),
left: item.x,
top: item.y,
child: Text(item.name),
),
],
),
)
Supported Layouts #
ColumnRowWrapStack
Migrating from v0.3.x #
v0.4.0 changes the enabled parameter from bool (default true) to bool? (default null):
null(new default) — auto-detects reduced motion fromMediaQuery.disableAnimationstrue— always animate (previous default behavior)false— never animate
If you relied on the default enabled: true and don't want auto reduced-motion, pass enabled: true explicitly.
Migrating from v0.1.0 #
v0.2.0 renamed the scale transition parameters for consistency:
ScaleIn.beginScale→ScaleIn.scaleScaleOut.endScale→ScaleOut.scale
API Reference #
MotionLayout #
| Parameter | Type | Default | Description |
|---|---|---|---|
child |
Widget |
required | A Column, Row, Wrap, or Stack |
duration |
Duration |
300ms | Move animation duration (fallback for enter/exit transitions) |
curve |
Curve |
Curves.easeInOut |
Animation curve (fallback for per-type curves) |
enterTransition |
MotionTransition? |
FadeIn() |
Transition for entering children |
exitTransition |
MotionTransition? |
FadeOut() |
Transition for exiting children |
clipBehavior |
Clip |
Clip.hardEdge |
How to clip during animation |
enabled |
bool? |
null |
null = auto-detect reduced motion, true = always animate, false = disable |
moveThreshold |
double |
0.5 |
Minimum position delta (logical pixels) to trigger a move animation |
transitionDuration |
Duration? |
null |
Duration for enter/exit transitions (falls back to duration) |
staggerDuration |
Duration |
Duration.zero |
Delay between each child's animation start |
staggerFrom |
StaggerFrom |
.first |
Direction of stagger cascade (.first, .last, .center) |
spring |
MotionSpring? |
null |
Spring physics for move animations (overrides curve/moveCurve) |
moveCurve |
Curve? |
null |
Curve override for move animations (falls back to curve) |
enterCurve |
Curve? |
null |
Curve override for enter transitions (falls back to curve) |
exitCurve |
Curve? |
null |
Curve override for exit transitions (falls back to curve) |
onAnimationStart |
VoidCallback? |
null |
Called when any animation starts |
onAnimationComplete |
VoidCallback? |
null |
Called when all animations complete |
onChildEnter |
ValueChanged<Key>? |
null |
Called when a child begins entering |
onChildExit |
ValueChanged<Key>? |
null |
Called when a child begins exiting |
MotionSpring #
| Preset | Stiffness | Damping | Mass | Character |
|---|---|---|---|---|
MotionSpring.gentle |
120 | 20 | 1 | Soft, minimal bounce |
MotionSpring.smooth |
200 | 22 | 1 | Natural feel |
MotionSpring.bouncy |
300 | 15 | 1 | Visible overshoot |
MotionSpring.stiff |
400 | 30 | 1 | Settles quickly |
Built-in Transitions #
| Class | Description |
|---|---|
FadeIn / FadeOut |
Opacity fade |
SlideIn / SlideOut |
Fractional slide (default offset: Offset(0, 0.15)) |
ScaleIn / ScaleOut |
Scale (default scale: 0.8) |
FadeSlideIn / FadeSlideOut |
Combined fade + slide |
FadeScaleIn / FadeScaleOut |
Combined fade + scale |
SizeIn / SizeOut |
Accordion/expand-collapse size transition |
Transition Composition #
Use the + operator to combine any transitions:
const FadeIn() + const SlideIn() // fade + slide
const FadeOut() + const ScaleOut(scale: 0.5) // fade + scale
const FadeIn() + const SlideIn() + const ScaleIn() // triple combo
Or use the constructor directly:
const ComposedTransition([FadeIn(), SlideIn(), ScaleIn()])
Custom Transitions #
Extend MotionTransition:
class MyTransition extends MotionTransition {
const MyTransition();
@override
Widget build(BuildContext context, Animation<double> animation, Widget child) {
return RotationTransition(turns: animation, child: child);
}
}
Troubleshooting #
Animations not working #
Ensure all children have unique Keys. Without keys, Flutter cannot track which children were added, removed, or reordered.
Unsupported layout type #
MotionLayout only supports Column, Row, Wrap, and Stack as the direct child. Other layout widgets are not supported.
Children overlap during animation #
Adjust the clipBehavior parameter. The default Clip.hardEdge clips overflowing children. Use Clip.none to allow children to paint outside the layout bounds during animation.
Performance with many items #
For large lists where you temporarily need to disable animations (e.g. during bulk updates), set enabled: false to skip animation processing entirely.
Accessibility #
Reduced Motion #
Animations are automatically disabled when the user has enabled "Reduce motion" in their system accessibility settings. MotionLayout reads MediaQuery.disableAnimations by default (when enabled is null).
To override the system setting:
// Always animate (ignore system preference):
MotionLayout(enabled: true, child: Column(children: [...]))
// Always disable (force instant layout):
MotionLayout(enabled: false, child: Column(children: [...]))
Screen Readers #
Exiting children are automatically wrapped in ExcludeSemantics and IgnorePointer, so screen readers skip disappearing elements and users cannot interact with them during exit animations.
How It Works #
FLIP = First, Last, Invert, Play
- First — Before layout changes, capture child positions
- Diff — Key-based diffing with LIS (Longest Increasing Subsequence) for minimal move detection
- Last — After Flutter lays out the new state, capture new positions
- Invert — Apply a
Transform.translateequal to (old position - new position) - Play — Animate the transform to zero, children visually glide to their new spots
Exit animations keep removed children in the tree until the animation completes. Enter animations fade/slide/scale new children in.
License #
MIT