skip to content

In Flutter, which Curves presets overshoot the 0.0-to-1.0 range, and what happens to the tweens and widgets they drive?

level: middleimportance: nice to knowfreq 22%

answer

  1. endpoints fixed, middle may escape
  2. Back and elastic families
  3. Tween extrapolates past end
  4. Opacity asserts, FadeTransition clamps
  5. curves reject input outside 0-1

basics

~20 s

Curves.easeInBack, easeOutBack, easeInOutBack and the elastic curves leave 0.0-1.0 between fixed endpoints. Tweens then extrapolate past begin or end: fine for scale or offset, but the Opacity widget, a downstream curve and TweenSequence assert on it.

solid answer

~40 s

Every `Curve` must map 0.0 to 0.0 and 1.0 to 1.0 - `CurvedAnimation` checks this in debug with an *Invalid curve endpoint* error - but values in between may leave the range. `Curves.easeOutBack` overshoots past 1.0 once; `easeInBack` dips below 0.0 first; `easeInOutBack` does both; `elasticIn`, `elasticOut` and `elasticInOut` oscillate across the bounds. The standard ease presets, `fastOutSlowIn` and the bounce curves stay inside. A `Tween<double>` simply extrapolates, so a scale from 0.6 to 1.0 briefly passes 1.0, which is the intended pop. Trouble is downstream: the `Opacity` widget asserts its value is within 0-1, a second curve or a `TweenSequence` asserts its input is within 0-1, and a size driven below zero reaches layout as a negative constraint. `FadeTransition` and `ColorTween` clamp instead.

code

dart · 12 lines
dart
// In initState, with an AnimationController named _controller.
// Scale may overshoot; opacity must not.
_scale = _controller
    .drive(CurveTween(curve: Curves.easeOutBack))
    .drive(Tween<double>(begin: 0.6, end: 1.0));
_opacity = _controller.drive(CurveTween(curve: Curves.easeOut));

// In build:
// ScaleTransition(
//   scale: _scale,
//   child: FadeTransition(opacity: _opacity, child: badge),
// )

go deeper

for a junior

Recall that the Back and elastic curves go past their end values before settling, and that the ordinary ease curves do not.

for a middle

Explain the curve contract: endpoints pinned, input 0-1 asserted, middle free. Then say which consumers extrapolate, which clamp and which assert.

for a senior

Place overshoot where it is tolerated, split bounded properties onto their own curves, and recognise the curve, TweenSequence and Opacity assertions when they appear in debug runs.

for a principal

Set a convention for where overshooting curves may appear in shared animation helpers so that one designer request cannot break bounded properties across screens.

## The contract a Curve must keep A **`Curve`** in Flutter maps a progress value `t` in `0.0`-`1.0` to an output. Its contract is narrow: - **the endpoints are pinned**: `transform(0.0)` returns `0.0` and `transform(1.0)` returns `1.0` - the base `Curve.transform` passes those two values straight through, and `CurvedAnimation` asserts in debug builds (*Invalid curve endpoint*) if a curve that overrides `transform` breaks this; - **the input must be in range**: for any other `t`, the base `transform` asserts that `t` lies within `0.0`-`1.0` (*parametric value ... is outside of [0, 1] range*); - **the output in between is free**. Nothing requires the middle of a curve to stay between `0.0` and `1.0`. That last point is what makes spring-like pops possible, and what surprises people. ## Which presets leave the range | Preset | Behaviour between the endpoints | |---|---| | `Curves.easeOutBack` | rises past `1.0` once, then settles back | | `Curves.easeInBack` | dips below `0.0` first, then rises | | `Curves.easeInOutBack` | dips below `0.0`, then rises past `1.0` | | `Curves.elasticIn` | oscillates across `0.0`, growing | | `Curves.elasticOut` | oscillates across `1.0`, shrinking | | `Curves.elasticInOut` | oscillates at both ends | | `ease`, `easeIn`, `easeOut`, `easeInOut`, `fastOutSlowIn` | stay within range | | `bounceIn`, `bounceOut`, `bounceInOut` | stay within range - they bounce off the bound, not past it | The Back presets are `Cubic` curves whose control points have a `y` outside `0`-`1` (for example `Curves.easeOutBack` is `Cubic(0.175, 0.885, 0.32, 1.275)`); a custom `Cubic` with such control points overshoots the same way. ## What an overshooting value does downstream | Consumer | Behaviour with a value outside 0-1 | |---|---| | `Tween<double>`, `Tween<Offset>` | extrapolates linearly past `begin` or `end` | | `ColorTween` | `Color.lerp` clamps each channel, so the color stops at the end color | | `FadeTransition` | its render object clamps opacity when converting to alpha | | `Opacity` widget | constructor asserts the value is within `0.0`-`1.0` | | another `Curve` or `CurveTween` | asserts its input is within `0.0`-`1.0` | | `TweenSequence` | asserts its `t` is within `0.0`-`1.0` | | a size fed to layout | a negative width or height fails `BoxConstraints` validation | So `Tween<double>(begin: 0.6, end: 1.0)` at an overshot `t` of `1.1` yields `1.04` - exactly the pop a designer asked for - while the same value passed straight into `Opacity` fails in debug. ## Placing an overshoot safely 1. Put the overshooting curve **last** in the chain of curves, directly before a tween that tolerates extrapolation (scale, offset, rotation). 2. Drive properties that must stay bounded, such as opacity, from a **separate** non-overshooting curve on the same controller. 3. Inside a stagger, put overshoot on the **window's own curve** (`Interval(0.2, 0.8, curve: Curves.easeOutBack)`) rather than on the parent that feeds the `Interval`. 4. Inside a `TweenSequence`, put overshoot on an **item's** `chain`, never on the sequence's parent. ## Diagnosing an overshoot assertion When a debug run stops with *parametric value ... is outside of [0, 1] range*, the failing frame belongs to the curve that received the bad input, not the one that produced it. Walk the pipeline backwards from that curve: the culprit is the nearest upstream curve from the Back or elastic families, or a `CurvedAnimation` whose parent is itself curved. An assertion from the `Opacity` constructor points the same way, at whatever feeds its `opacity`. In release builds the asserts are stripped, so the same bug shows up as a value that briefly escapes its range instead of an error - which is why it should be fixed in debug, where it is loud. ## Mistakes interviewers listen for - Claiming every preset stays within `0.0`-`1.0`. - Expecting `Tween` to clamp to `begin` and `end`. - Assuming the bounce curves overshoot like the Back curves. - Treating overshoot as purely visual when several framework classes assert on it. - Writing a custom curve that overrides `transform` and returns something other than `0.0` and `1.0` at the ends.

  • When does CurvedAnimation throw 'Invalid curve endpoint' for a custom curve?
    In debug builds `CurvedAnimation` checks that its active curve maps 0.0 to near 0.0 and 1.0 to near 1.0. The base `Curve.transform` already passes both endpoints through, so a subclass that only overrides `transformInternal` cannot fail this; the error comes from a curve that overrides `transform` itself and returns another value at an end, which would make the animation jump when it settles.
  • How do you get an overshooting pop on a widget whose opacity also animates?
    Drive the two properties from separate curves on the same controller: an overshooting curve such as `Curves.easeOutBack` into a scale `Tween`, and a bounded curve such as `Curves.easeOut` for opacity. `FadeTransition` would clamp an overshot opacity anyway, but a raw `Opacity` widget, a downstream curve or a `TweenSequence` would assert.

saying these in an interview costs you the question

  • Every Curves preset keeps its output between 0.0 and 1.0.
  • Tween clamps its output to begin and end automatically.
  • Curves.bounceOut overshoots past 1.0 like easeOutBack.
  • Overshoot is purely visual; no framework class asserts on it.
  • A curve's endpoints may map anywhere as long as the middle looks right.