motion_number

Numbers that arrive instead of appearing. Every digit rolls on its own timeline, the motion knows whether the value went up or down, and seven styles share one animation controller.

MotionNumber(value: 131890)

That is the whole API for the common case. Everything below is opt-in.


Why not just tween a double?

Most animated counters interpolate a double and re-render value.toString() every frame. Three things go wrong:

Defect Cause What you see
Layout jitter Proportional fonts give 1 and 8 different widths The row twitches every frame
Meaningless motion The string is interpolated, not the digits ₹9,999 → ₹10,000 scrambles all five glyphs
No direction A number is just a number Nothing says it went up

motion_number treats a formatted number as a stable list of glyph slots. Slots are aligned from the right, so 999 → 1000 animates one new digit in rather than churning four. Separators are slots too, so a new comma slides in instead of snapping.


The seven styles

rolling odometer slotMachine
rolling odometer slot machine
Shortest path, direction-aware. Clean and financial. The default. A mechanical dial, always continuous, carrying 9 → 0. Extra revolutions before settling, staggered hard.
elastic wave shuffle
elastic wave shuffle
Rolling with overshoot and a small scale pop. Rolling with a sinusoidal offset across the row. Random digits, then a settle. The "decrypting" look.
flip
flip
Split-flap board: the old digit folds away, the new one falls in.
MotionNumber(value: 131890, style: NumberMotionStyle.flip)

Install

dependencies:
  motion_number: ^0.1.0
import 'package:motion_number/motion_number.dart';

Minimal example

int _value = 1234;

MotionNumber(value: _value)

Change _value inside setState and it animates. No controller to own, no dispose to remember.


Indian currency, with a direction-aware delta

₹1,24,350 → ₹1,31,890, and the percentage that goes with it:

Row(
  children: <Widget>[
    MotionNumber(
      value: amount,
      formatter: IntlFormatter.currency(
        locale: 'en_IN',
        symbol: '₹',
        decimalDigits: 0,
      ),
      directionColors: const DirectionColors.greenUp(),
    ),
    const SizedBox(width: 8),
    const MotionDelta(),
  ],
)

MotionDelta renders ↑ +6.06% with no wiring between the two widgets: MotionNumber publishes each transition into an inherited scope, and any MotionDelta below it reads from there. Pass transition: explicitly if you would rather not use the scope.

Two things here that a fixed-width grouper cannot do: en_IN groups the leading digits in pairs (1,24,350, not 124,350), and the delta knows the value rose. Percentage change from zero is undefined, so a transition starting at 0 shows the absolute delta instead of ∞% or a silently wrong 100%.


Formatters

Formatter Input Output
PlainFormatter() 131890 131,890
PlainFormatter(grouped: false) 131890 131890
IntlFormatter.decimal(locale: 'en_IN') 124350 1,24,350
IntlFormatter.currency(locale: 'en_IN', symbol: '₹', decimalDigits: 0) 124350 ₹1,24,350
CompactFormatter() 1243500 1.2M
CompactFormatter(scale: CompactScale.indian) 1243500 12.4L

IntlFormatter wraps any intl NumberFormat, which is why locale auto-detection and currency conversion stay out of scope — pass whichever format you want. Implement NumberTextFormatter for anything else.


Parameters

MotionNumber

Parameter Type Default What it does
value num required The value to display
style NumberMotionStyle rolling Which built-in motion to use
motion DigitMotion? null A custom strategy, overriding style
formatter NumberTextFormatter? PlainFormatter() How the value becomes text
duration Duration? the style's own How long a transition takes
curve Curve? the style's own Easing applied to the transition
textStyle TextStyle? ambient Style for the digits
tabularFigures bool true Requests tnum, making digit widths exact
animateOnFirstBuild bool false Whether the first build animates from zero
directionColors DirectionColors? null Tints digits by direction while they move
onTransition ValueChanged<ValueTransition>? null Fires when a transition begins

animateOnFirstBuild is off by default so that opening a dashboard does not animate every number on it at once.

MotionDelta

Parameter Type Default What it does
transition ValueTransition? nearest scope's Which transition to render
decimalDigits int 2 Digits after the decimal point
showArrow bool true Whether to prefix /
textStyle TextStyle? theme, then ambient Style for the text
directionColors DirectionColors? theme's Colours applied by direction
deltaBuilder Widget Function(BuildContext, ValueTransition)? null Replaces the default rendering entirely

Theming

MotionNumberTheme is a ThemeExtension, so defaults live with the rest of your theme rather than at every call site:

MaterialApp(
  theme: ThemeData(
    extensions: const <ThemeExtension<dynamic>>[
      MotionNumberTheme(
        style: NumberMotionStyle.odometer,
        duration: Duration(milliseconds: 700),
        directionColors: DirectionColors.greenUp(),
      ),
    ],
  ),
)

Resolution order is explicit argument, then theme, then the style's own default. DirectionColors.redUp() is there for markets where red means a rise.


Accessibility

A screen reader announces the formatted value — 131,890 — and nothing else. The 09 strip that each digit slides through is wrapped in ExcludeSemantics; without that, every digit would read out as "zero one two three four five six seven eight nine".

MediaQuery.disableAnimationsOf is honoured: when a user has asked their platform to reduce motion, every slot renders at t = 1.0 on the first frame. The value is correct immediately, and nothing moves.


Performance

One AnimationController drives the whole widget however many digits it has — per-digit independence comes from stagger, not from twelve tickers. The 09 strip is built once and reused until the text style or cell size changes, each digit sits behind a RepaintBoundary, and cell width is measured from the widest glyph so the row never reflows mid-animation.


Status

0.x, and the constraint above should be read that way: the API is not frozen, and the parameter tables may still change before 1.0.0. Behaviour is covered by 155 tests, plus a golden suite over all seven styles.

Issues and pull requests: SwarupFutane/motion_numbers.

License

MIT — see LICENSE.

Libraries

motion_number
Direction-aware animated numbers for Flutter.