Expand description
ScrollController: a cloneable, reactive-free handle onto a
ScrollView or a ListView — the
programmatic-scroll seam.
App code keeps a handle in its state, attaches it with
ScrollView::controller or
ListView::controller, and drives or reads
the surface from anywhere on the UI thread, inside a dispatch or not:
- Writes are recorded, not applied.
ScrollController::jump_to,ScrollController::animate_toandScrollController::scroll_to_itemqueue a command the attached widget drains at the start of its next layout or paint (whichever runs first — aListViewalso drains at its next rebuild, before it plans the window, so the jump’s frame already materializes the rows it lands on), so the clamp is always taken against the freshly laid-out extent — a jump issued in the same handler that appended content lands on the new bottom, not the old one. A command queued before any surface attaches (or before the attached one has laid out) waits for the first layout. Recording raisesfrust_core::mark_pending_result_flushso a frame-gated shell runs the frame that applies it even when nothing else changed (thePanZoomController/NavigatorControllerprecedent). - Reads are the last published snapshot.
ScrollController::offset,ScrollController::max_offsetandScrollController::viewport_extentreturn what the attached surface last published — after every layout, paint and event pass that changed it — andScrollController::on_changelisteners hear each newScrollInfoas it is published. - One surface per handle. The most recent attach wins: a second surface attaching the same handle takes it over and the first stops draining or publishing; dropping the attached widget detaches it.
is_animatingtracks the current binding, never a stale one.ScrollController::is_animatingis true only while the currently attached binding’s surface has a live controller-drivenScrollController::animate_totween running — never a snapshot of whatever the handle last happened to report. Both places current-ness changes reconcile it so a detached or superseded binding can never leave it stuck true:ScrollController::bindresets it for the newly attached binding, and dropping the binding that was still current clears it (impl Drop for ScrollBinding) even when the widget itself never got to callScrollController::is_animating’s setter — covering a tween whose surface is dropped, taken over, or rebound mid-flight. A widget whose tween genuinely survives a rebind to a different handle (the tween belongs to the widget, not the handle) republishes its live state onto the freshly bound handle right after binding, so a real in-flight tween is still reported, just under the new current binding instead of the old one.- Item commands are list-only.
ScrollController::scroll_to_itemnames a row by itsChildKey, which only a keyedListViewcan resolve. The queue therefore holds a wider crate-internal command type than the offset-onlyScrollCommandaScrollViewmatches on: theScrollViewdrain filters item commands out (ignoring each with a debug-build log) and keeps the offset ones in recording order, soScrollViewnever sees — and never has to match on — a variant it cannot act on. - The queue is bounded, not an unbounded backlog. Offset commands
are grouped into segments by the item commands between them, and
within a segment a
jump_tosupersedes every earlier offset command (it cancels any tween and sets the position outright) while ananimate_tosupersedes an earlieranimate_tobut keeps a precedingjump_to, so the tween still starts from the jumped-to position. A segment therefore holds at most one jump and one animate in whatever order they were recorded — a tight burst (a pointer-drag thumb, say) or an alternating jump/animate loop both leave at most two entries — and superseding never crosses aScrollController::scroll_to_item(an offset/item interleaving stays in exact recording order). Item commands keep their own ordered channel but are capped at [ITEM_COMMAND_CAP] entries; recording one past the cap drops the oldest queued item command (debug-build log) rather than growing further, so the whole queue never exceeds the cap plus two offset entries per segment. One behavioural consequence: a supersededjump_tothat would, had it actually applied, transiently cross aListViewnear-start/near-end threshold never does — the edge callbacks evaluate only the surviving target, not every value a caller recorded. - The pre-attach backlog survives a bind.
ScrollController::binddoes not clear the queue: a command recorded before any surface attaches (or before the newly attached one has laid out) waits for that surface’s first layout, per the apply contract above. The hazard this trades for: a handle driven in a loop while nothing is attached cannot grow without bound either — superseding and the item cap still apply — but it does accumulate up to that bound (at most one jump and one animate per segment, up to [ITEM_COMMAND_CAP] item commands) rather than draining as it is issued.
See docs/WIDGETS_ARCHITECTURE.md’s Scroll Physics for the surface the
handle drives.
Structs§
- Animate
To - The duration/curve
ScrollController::animate_toeases a programmatic scroll through. Both fields are required rather than defaulted — Flutter’sScrollController.animateTokeepsduration/curverequired for the same reason: an app names the motion it wants rather than inheriting a guessed one. - Scroll
Controller - A cloneable handle onto a
ScrollViewor aListView: drive it withjump_to/animate_to(and, on a keyed list,scroll_to_item), read where it stands withoffset/max_offset/viewport_extent, and observe it withon_change. Every clone shares one state, so the handle an app keeps in its state and the one attached to the view are the same. See the module docs for the apply/publish contract. - Scroll
Subscription - Keeps an
ScrollController::on_changelistener subscribed; dropping it unsubscribes. Holds the handle weakly, so it never keeps a controller alive.
Enums§
- Item
Alignment - Where
ScrollController::scroll_to_itemlands a row in the viewport.