skip to content

In Flutter, how do an AnimationController's forward, reverse, animateTo and repeat differ, and how long does forward take from a mid value?

level: middleimportance: should knowfreq 45%

answer

  1. forward and reverse run to the bounds
  2. animateTo any target, optional curve
  3. time scales with distance left
  4. reverseDuration for the way back
  5. setting value stops the animation

basics

~20 s

forward runs to upperBound and reverse to lowerBound; animateTo and animateBack run to any target with an optional curve; repeat loops. Without an explicit duration, the run time scales with the distance left, so forward from 0.5 takes half of duration.

solid answer

~40 s

`forward({from})` drives `value` to `upperBound` with status `forward`, and `reverse({from})` drives it to `lowerBound` with status `reverse`, using `reverseDuration` if set. `animateTo(target, {duration, curve})` and `animateBack` run to any value; `curve` defaults to `Curves.linear`. `repeat({min, max, reverse, period, count})` loops until stopped, or `count` times. `toggle()` reverses if the controller is moving or has finished forward, otherwise goes forward. When no explicit duration is passed, the controller scales the time by the fraction of the range left: `forward()` from 0.5 on a 0-to-1 controller with a 400 ms duration takes 200 ms, so a reversal mid-way is not slower than the rest of the motion. Setting `value` directly stops the animation, jumps, notifies listeners and recomputes status. `stop()` halts without changing value or status.

code

dart · 35 lines
dart
import 'package:flutter/material.dart';

class HoldToRecord extends StatefulWidget {
  const HoldToRecord({super.key});

  @override
  State<HoldToRecord> createState() => _HoldToRecordState();
}

class _HoldToRecordState extends State<HoldToRecord> with SingleTickerProviderStateMixin {
  late final AnimationController _press = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 300),
    reverseDuration: const Duration(milliseconds: 150),
  );

  @override
  void dispose() {
    _press.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => _press.forward(),   // time scales with distance left
      onTapUp: (_) => _press.reverse(),     // uses reverseDuration
      onTapCancel: () => _press.reverse(),
      child: ScaleTransition(
        scale: Tween<double>(begin: 1.0, end: 1.2).animate(_press),
        child: const Icon(Icons.mic, size: 48),
      ),
    );
  }
}

go deeper

for a junior

Recall forward, reverse, animateTo and repeat and which bound or target each runs to.

for a middle

Explain duration scaling by remaining fraction, reverseDuration, the status each method reports, and stop versus setting value.

for a senior

Use these rules to build press, release and interruption behaviour that stays at constant speed and reports the status listeners expect.

for a principal

Choose controller semantics deliberately in shared components so status-driven logic behaves the same across the app.

## The value and its bounds An **`AnimationController`** holds a `double` **`value`** between **`lowerBound`** and **`upperBound`** — 0.0 and 1.0 unless you pass others — and a **`status`** (`dismissed`, `forward`, `reverse`, `completed`). Its methods start a run from the current value towards a target, each returning a **`TickerFuture`** that completes when the run finishes normally. ## The driving methods | Method | Runs to | Status while running | Status at the end | |---|---|---|---| | `forward({from})` | `upperBound` | `forward` | `completed` | | `reverse({from})` | `lowerBound` | `reverse` | `dismissed` | | `toggle({from})` | the opposite bound | depends on direction | `completed` or `dismissed` | | `animateTo(target, {duration, curve})` | `target` | `forward` | `completed` | | `animateBack(target, {duration, curve})` | `target` | `reverse` | `dismissed` | | `repeat({min, max, reverse, period, count})` | loops between `min` and `max` | `forward`, or alternating with `reverse` | only ends if `count` is set | Details worth knowing: - **`from`** on `forward`, `reverse` and `toggle` first sets `value`, so `forward(from: 0.0)` restarts from the beginning. - **`animateTo` reports `forward` even when the target is below the current value**, and ends `completed` even if the target is not `upperBound`; `animateBack` mirrors that with `reverse` and `dismissed`. Choose between them by the status you want listeners to see. - **`curve`** on `animateTo` and `animateBack` defaults to `Curves.linear`; `forward` and `reverse` always run linearly, and easing comes from a curved animation layered on top. - **`toggle()`** acts like `reverse()` when the status is `forward` or `completed`, and like `forward()` otherwise. - **`repeat`**'s `period` defaults to `duration`; without `count`, its `TickerFuture` never completes. ## How long a run takes When you do not pass an explicit `duration` to the call, the controller uses **`duration`** (or **`reverseDuration`** when running in reverse and one is set) **scaled by the fraction of the range still to cover**: 1. Range = `upperBound - lowerBound`. 2. Remaining fraction = `|target - value| / range`. 3. Run time = direction's duration × remaining fraction. So on a 0-to-1 controller with `duration: 400 ms`, `forward()` from 0.5 takes **200 ms**, and `reverse()` from 0.9 with `reverseDuration: 200 ms` takes **180 ms**. This keeps the speed constant when the user reverses a motion mid-way, which is why a pressed-and-released button feels consistent. An explicit `duration` passed to `animateTo` is used as-is. ## Choosing a method - **Between two resting states** (pressed and released, open and closed): `forward` and `reverse`, or `toggle`, so status tells you which end you are at. - **To an arbitrary point** (a level meter, a progress value, a drag release position): `animateTo`, with an explicit `duration` when each hop should take the same time regardless of distance. - **Back to a point with reverse semantics**: `animateBack`, so listeners see `reverse` and then `dismissed`. - **Continuous motion**: `repeat`, with `min` and `max` to loop over part of the range and `count` for a fixed number of beats. ## Stopping and jumping - **`stop({canceled = true})`** halts the ticker. `value` and `status` are unchanged, and no listener is notified; with `canceled: true` the run's `TickerFuture` never completes and its `orCancel` future fails. - **`value = x`** stops any run, clamps and sets the value, **notifies listeners**, and recomputes status: `dismissed` at `lowerBound`, `completed` at `upperBound`, otherwise the last direction. - **`reset()`** is `value = lowerBound`. ## A dictation-app example The record button grows while the user holds it and shrinks when they release: ```dart void _onPressDown() => _press.forward(); void _onPressUp() => _press.reverse(); ``` With `duration: 300 ms` and `reverseDuration: 150 ms`, a quick tap released at value 0.4 takes 60 ms to shrink back — 40% of 150 ms — instead of a full 150 ms, so the button never lags behind the finger. A level meter uses `animateTo(level, duration: const Duration(milliseconds: 80))` because each new microphone level is an arbitrary target, not a bound. ## Accessibility behaviour By default a controller's `animationBehavior` is `AnimationBehavior.normal`: when the platform asks apps to disable animations, runs started by `forward`, `reverse`, `toggle`, `animateTo` and `animateBack` take **5% of their duration** rather than zero; the source keeps a short non-zero run so that code restarting an animation on completion cannot spin in an endless loop. `AnimationBehavior.preserve` opts a controller out.

  • Why does animateTo(0.0) from 1.0 end with status completed rather than dismissed?
    `animateTo` always runs in the forward direction for status purposes, whatever the target. When the run finishes, the controller reports `completed` for a forward run. To end in `dismissed`, and to show `reverse` while moving, use `animateBack(0.0)` or `reverse()`.
  • What is the difference between stop() and setting value?
    `stop()` only halts the ticker: value and status stay as they are and listeners are not notified. Setting `value` also stops the run, but then clamps and stores the new value, notifies listeners, and recomputes status from the new value.

saying these in an interview costs you the question

  • forward() from a mid value always takes the full duration.
  • animateTo below the current value reports status reverse.
  • stop() resets the controller to its lower bound.
  • Setting value directly keeps the running animation going.
  • repeat() completes its future after the first cycle.