In Flutter, which Curves presets overshoot the 0.0-to-1.0 range, and what happens to the tweens and widgets they drive?
answer
- endpoints fixed, middle may escape
- Back and elastic families
- Tween extrapolates past end
- Opacity asserts, FadeTransition clamps
- curves reject input outside 0-1
basics
~20 sCurves.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 sEvery `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// 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
Recall that the Back and elastic curves go past their end values before settling, and that the ordinary ease curves do not.
Explain the curve contract: endpoints pinned, input 0-1 asserted, middle free. Then say which consumers extrapolate, which clamp and which assert.
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.
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.