Motor

Pub Version Coverage lintervention_badge Bluesky

A unified motion system that brings together physics-based springs, duration-based curves, and Flutter's animation system under one consistent API.

Title animation

Features 🎯

  • 🎨 Unified Motion API - One consistent interface for springs, curves, and custom motions
  • 💡 Physics & Duration Based - Choose between spring physics or traditional duration/curve animations
  • 🍎 Apple Design System - Built-in CupertinoMotion presets matching iOS animations
  • 🎨 Material Design 3 - MaterialSpringMotion tokens following Google's motion guidelines
  • 📱 Multi-dimensional - Animate complex types like Offset, Size, and Rect with independent physics per dimension
  • 🔄 Interactive Widgets - Motion-driven draggable widgets with natural return animations
  • 🎯 Flutter Integration - Works seamlessly with existing Flutter animation patterns

Try it out

Open Example

Installation 💻

❗ In order to start using Motor you must have the Dart SDK installed on your machine.

Add to your pubspec.yaml:

dependencies:
  motor: ^1.0.0-dev.0

Or install via dart pub:

dart pub add motor

Usage 💡

Motion

The core of Motor's unified motion system is the Motion class. It represents the type of motion that will drive your animation, whether physics-based or duration-based.

// Duration-based motion (traditional Flutter approach)
final linear = Motion.linear(Duration(seconds: 1));

final withCurve = Motion.curved(Duration(seconds: 1), Curves.easeInOut);

// Physics-based motion (natural, responsive)
final spring = CupertinoMotion.bouncy(); // Or `Motion.bouncySpring()`
final material = MaterialSpringMotion.standardSpatialDefault();

Motor provides several motion types out of the box, with the ability to create custom motions by implementing the Motion interface:

  • CurvedMotion - Traditional duration-based motion with curves. Perfect for predictable, timed animations.
  • LinearMotion - Like CurvedMotion but always linear.
  • NoMotion - Holds at the target value for an optional duration.
  • SpringMotion - Physics-based motion using Flutter SDK's SpringDescription. Provides natural, responsive animations that feel alive.
  • CupertinoMotion - Predefined spring configurations matching Apple's design system.
  • MaterialSpringMotion - Material Design 3 spring motion tokens for expressive animations.

This unified approach means you can easily switch between physics and duration-based animations without changing your widget code.

CupertinoMotion

CupertinoMotion is a subclass of SpringMotion that provides predefined spring configurations matching Apple's design system. These motions are designed to feel natural and familiar to iOS users, as they mirror the spring animations used throughout Apple's platforms.

CupertinoMotion offers several predefined constants that correspond to SwiftUI's animation presets:

  • CupertinoMotion() - The default iOS spring with smooth motion and no bounce
  • .smooth() - A smooth spring animation with no bounce, ideal for subtle transitions
  • .bouncy() - A bouncy spring with higher bounce, perfect for playful interactions
  • .snappy() - A snappy spring with small bounce that feels responsive
  • .interactive() - An interactive spring with lower response, designed for user-driven animations

You can also create custom CupertinoMotion instances:

final customMotion = CupertinoMotion(
  duration: Duration(milliseconds: 600),
  bounce: 0.3,
);

Since CupertinoMotion extends SpringMotion (which extends Motion), you can use it directly wherever a Motion is expected.

MaterialSpringMotion

MaterialSpringMotion provides Material Design 3 spring motion tokens for creating expressive and natural animations that follow Google's design guidelines. The tokens are organized into two main categories with three speed variants each:

Spatial Motion - For animating position, size, and layout changes:

  • .standardSpatialFast() - Quick spatial animations (damping: 0.9, stiffness: 1400)
  • .standardSpatialDefault() - Balanced spatial animations (damping: 0.9, stiffness: 700)
  • .standardSpatialSlow() - Gentle spatial animations (damping: 0.9, stiffness: 300)
  • .expressiveSpatialFast() - Dynamic spatial with bounce (damping: 0.6, stiffness: 800)
  • .expressiveSpatialDefault() - Moderate expressive spatial (damping: 0.8, stiffness: 380)
  • .expressiveSpatialSlow() - Gentle expressive spatial (damping: 0.8, stiffness: 200)

Effects Motion - For animating visual properties like opacity and color:

  • .standardEffectsFast() - Quick effects animations (damping: 1, stiffness: 3800)
  • .standardEffectsDefault() - Balanced effects animations (damping: 1, stiffness: 1600)
  • .standardEffectsSlow() - Gentle effects animations (damping: 1, stiffness: 800)
  • .expressiveEffectsFast() - Quick expressive effects (damping: 1, stiffness: 3800)
  • .expressiveEffectsDefault() - Moderate expressive effects (damping: 1, stiffness: 1600)
  • .expressiveEffectsSlow() - Gentle expressive effects (damping: 1, stiffness: 800)

You can also create custom MaterialSpringMotion instances:

final customMaterial = MaterialSpringMotion(
  damping: 0.8,
  stiffness: 500,
);

These motion tokens follow the Material Design 3 Motion Guidelines and are designed to create consistent, expressive animations across Material Design applications.

Simple Animation

Use SingleMotionBuilder for basic, one-dimensional animations:

1D Hover example gif

SingleMotionBuilder(
  motion: CupertinoMotion.bouncy(),
  value: targetValue, // Changes trigger smooth spring animation
  builder: (context, value, child) {
    return Container(
      width: value,
      height: value,
      color: Colors.blue,
    );
  },
)

If you want to animate more complex types, such as Offset, Size, or Rect, you can use MotionBuilder and pass a so-called MotionConverter to it:

2D Redirection example gif

MotionBuilder(
  motion: CupertinoMotion.bouncy(),
  value: const Offset(100, 100),
  from: Offset.zero,
  converter: OffsetMotionConverter(),
  builder: (context, value, child) {
    return Transform.translate(
      offset: value,
      child: child,
    );
  },
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
  ),
)

For Material Design applications, you can use MaterialSpringMotion tokens:

MotionBuilder(
  motion: MaterialSpringMotion.expressiveSpatialDefault,
  value: const Offset(100, 100),
  from: Offset.zero,
  converter: OffsetMotionConverter(),
  builder: (context, value, child) {
    return Transform.translate(
      offset: value,
      child: child,
    );
  },
  child: Container(
    width: 100,
    height: 100,
    color: Colors.green,
  ),
)

Sequence Animations 🎬

Motor's sequence animations let you create complex, multi-phase animations with smooth transitions between states. Perfect for storytelling, onboarding flows, state machines, and complex UI transitions.

Note: The upcoming examples use the Dart 3.10 dot-shorthand syntax.

Motion Sequences

A MotionSequence defines a series of phases that your animation progresses through. Motor provides three types of sequences for different use cases:

1. State Sequences - Named Phases

Perfect for state machines, enums, or any named phase system:

enum ButtonState { idle, pressed, loading }

final MotionSequence<ButtonState, Offset> buttonSequence = .states({
  .idle: Offset(0, 0),
  .pressed: Offset(0, 5),
  .loading: Offset(10, 0),
}, motion: .bouncySpring());

2. Step Sequences - Ordered Progression

The most common sequence type for ordered progressions through values:

final MotionSequence<int, Color> colorSequence = MotionSequence.steps([
  Colors.red,
  Colors.yellow, 
  Colors.green,
  Colors.blue,
], motion: .smoothSpring(), loop: .seamless);

3. Spanning Sequences - Proportional Timing

For precise timing control where a single motion spans across positioned phases. Think of it like flexbox - phases at higher positions take proportionally more time to reach:

final logoSequence = MotionSequence.spanning({
  0.0: LogoState(opacity: 0),        // Start (0% of total time)
  1.0: LogoState(opacity: 1),        // 50% of total time
  2.0: LogoState(opacity: 1, text: 1), // 100% of total time
}, motion: .linear(Duration(seconds: 2)));

Loop Modes

Control how your sequences repeat:

  • LoopMode.none - Play once and stop
  • LoopMode.loop - Animate back to start and repeat
  • LoopMode.seamless - Treat first/last phases as identical for smooth circular loops
  • LoopMode.pingPong - Play forward then backward

Sequence Animation Widget

Use SequenceMotionBuilder to bring sequences to life:

enum LoadingState { idle, spinning, complete }

SequenceMotionBuilder<LoadingState, double>(
  sequence: .states({
    .idle: 0.0,
    .spinning: 2 * pi,
    .complete: 2 * pi,
  }, motion: .smoothSpring()),
  converter: .single,
  playing: true, // Auto-progress through phases
  currentPhase: currentState, // Or control manually
  onPhaseChanged: (phase) => print('Now in phase: $phase'),
  builder: (context, rotation, phase, child) {
    return Transform.rotate(
      angle: rotation,
      child: Icon(
        phase == .complete ? Icons.check : Icons.refresh,
        color: phase == .complete ? Colors.green : Colors.blue,
      ),
    );
  },
)

Manual vs Automatic Playback

Automatic Playback (playing: true):

  • Progresses through all phases automatically
  • Respects loop modes for continuous animation
  • Perfect for loading indicators, demonstrations

Manual Control (playing: false):

  • Only animates when currentPhase changes
  • Full control over phase transitions
  • Ideal for user-driven state changes, interactive tutorials

Individual Motion Per Phase

For ultimate control, specify different motions for each phase:

final complexSequence = MotionSequence<AppState, ButtonStyle>.statesWithMotions({
  .loading: (loadingStyle, .smoothSpring()),
  .error: (errorStyle, .bouncySpring()), // Extra bounce for attention
  .success: (successStyle, .curved(Duration(seconds: 2), Curves.ease)),
});

Advanced: Phase Motion Controllers

For maximum control, use PhaseMotionController directly:

final controller = PhaseMotionController<ButtonState, Offset>(
  motion: .smoothSpring(),
  vsync: this,
  converter: .offset,
  initialValue: .zero,
);

// Play a sequence
await controller.playSequence(buttonSequence);

// Check current state
if (controller.isPlayingSequence) {
  print('Current phase: ${controller.currentSequencePhase}');
  print('Progress: ${controller.sequenceProgress}');
}

Sequence animations work with any motion type - mix springs, curves, and custom motions within the same sequence for rich, expressive animations.

MotionConverter

One of Motor's key advantages is its ability to animate complex types with independent motion per dimension. While Flutter's basic animation system typically uses single animations with Tweens, Motor's unified motion system can simulate each dimension independently.

This is crucial for natural-feeling animations. For example, when animating a draggable icon, the user might fling it horizontally (high horizontal velocity) while it settles vertically (low vertical velocity). Traditional single-animation approaches would lose this dimensional independence, making the motion feel artificial.

MotionConverters solve this by breaking any type into multiple dimensions, allowing each dimension to follow the same motion pattern but with independent physics simulation.

This works with any motion type - whether you're using spring physics or duration-based curves, each dimension animates independently for maximum fidelity.

For often-used Flutter types, these are already implemented:

  • OffsetMotionConverter
  • SizeMotionConverter
  • RectMotionConverter
  • AlignmentMotionConverter

However, you might want your very custom type to be animated as well. For this, you can implement your own MotionConverter and pass it to the MotionBuilder constructor.

class My3DMotionConverter implements MotionConverter<Vector3> {
  @override
  List<double> normalize(Vector3 value) => [value.x, value.y, value.z];

  @override
  Vector3 denormalize(List<double> values) => Vector3(values[0], values[1], values[2]);
}

Widget build(BuildContext context) {
  return MotionBuilder(
    motion: CupertinoMotion.bouncy(),
    value: Vector3(100, 100, 100),
    converter: My3DMotionConverter(),
    // ...
  );
}

Or, just use MotionConverter directly and pass the converter functions to its constructor:

final converter = MotionConverter(
  normalize: (value) => [value.x, value.y, value.z],
  denormalize: (values) => Vector3(values[0], values[1], values[2]),
);

Motion Draggable

Motor includes a MotionDraggable widget that demonstrates the power of the unified motion system. You can drag widgets around the screen and watch them return with any motion type - from bouncy springs to smooth curves.

It works just like Flutter's Draggable widget and supports native DragTargets, but with motion-driven return animations and enhanced physics simulation.

Spring Draggable example gif

MotionDraggable(
  motion: CupertinoMotion.bouncy(),
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
  ),
  data: 'my-draggable-data',
)

Low-level Motion Control

For maximum control, Motor provides MotionController for complex types and SingleMotionController for one-dimensional animations. These controllers work with any motion type in the unified system.

final controller = MotionController(
  motion: CupertinoMotion.bouncy(), // or Motion.duration(), etc.
  vsync: this,
);

Motion controllers work similarly to Flutter's AnimationController but with key advantages:

  • Motion-agnostic: Switch between springs and curves without changing controller code
  • Velocity preservation: Maintains velocity when changing targets (crucial for natural motion)
  • Multi-dimensional: Each dimension can have independent physics simulation

Bounded vs. Unbounded Motion

In Flutter, the AnimationController can be either bounded or unbounded. MotionControllers come in both flavors as well, but they differ in key ways:

MotionController:
  • By default, MotionControllers are unbounded.
  • Unbounded MotionControllers don't have forward or reverse methods, since they don't make sense in multi-dimensional space.
BoundedMotionController:
  • requires you to specify a lowerBound and upperBound in the constructor.
  • exposes forward and reverse methods, which internally animate towards the upperBound and lowerBound respectively.
  • will clamp the animation value to be within the bounds, but they can still overshoot as part of their Motion simulation.

Custom Springs 🔧

For predefined spring configurations, see the CupertinoMotion section above.

You can also create completely custom springs:

// Using CupertinoMotion constructor
final mySpring = CupertinoMotion(
  duration: Duration(milliseconds: 500),
  bounce: 0.2,   // Bounce amount (-1 to 1)
);

// Or using Flutter SDK's SpringDescription directly
final customSpring = SpringMotion(
  SpringDescription.withDurationAndBounce(
    duration: Duration(milliseconds: 500),
    bounce: 0.2,
  ),
);

Acknowledgements

Motor's unified motion system builds upon excellent work from the Flutter community:

  • Spring physics implementation was partially adapted from and heavily inspired by fluid_animations
  • CupertinoMotion presets are designed to match Apple's SwiftUI animation system
  • The Motion abstraction unifies concepts from Flutter's animation framework with modern physics-based approaches

Libraries

motor
A unified motion system for Flutter - physics-based springs and duration-based curves under one API.