layout_motion 0.4.0 copy "layout_motion: ^0.4.0" to clipboard
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 #

pub package License: MIT

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 MotionLayout and you're done
  • FLIP technique — GPU-accelerated Transform animations (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 callbacksonAnimationStart, 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 #

  • Column
  • Row
  • Wrap
  • Stack

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 from MediaQuery.disableAnimations
  • true — 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.beginScaleScaleIn.scale
  • ScaleOut.endScaleScaleOut.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

  1. First — Before layout changes, capture child positions
  2. Diff — Key-based diffing with LIS (Longest Increasing Subsequence) for minimal move detection
  3. Last — After Flutter lays out the new state, capture new positions
  4. Invert — Apply a Transform.translate equal to (old position - new position)
  5. 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

2
likes
160
points
315
downloads

Publisher

unverified uploader

Weekly Downloads

Automatic FLIP layout animations for Flutter. Wrap any Column, Row, Wrap, or Stack to animate add, remove, and reorder with zero configuration.

Repository (GitHub)
View/report issues

Topics

#animation #layout #widget #flip #transition

Documentation

API reference

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter

More

Packages that depend on layout_motion