handleSample method
Feeds one scroll sample to the state machine.
Pure with respect to Flutter, which is what makes it safe to call from anywhere: a host that observes scrolling some other way than owning a ScrollController can build its own samples and drive the controller directly. handleNotification is the ready-made path for the common case of a NotificationListener.
The order of the rules matters, and each early return also decides whether the accumulator survives.
Implementation
void handleSample(GlassTabBarScrollSample sample) {
if (!_minimizes) return;
final previous = _baseline;
_baseline = sample.pixels;
// 1. Content that cannot scroll never minimizes — and content that shrinks
// below the threshold while minimized must expand, or the bar is
// stranded as a circle with nothing to scroll back.
if (sample.maxScrollExtent - sample.minScrollExtent <
kMinScrollableExtent) {
_accumulated = 0.0;
_setMinimized(false);
return;
}
// 2. At the resting edge, always expand — no threshold, no accumulation.
// `<=` rather than `==` so top rubber-band counts as "at the top":
// outOfRange is strict, so pixels == 0.0 is not out of range and would
// otherwise fall through to the accumulator below.
if (_atRestingEdge(sample)) {
_accumulated = 0.0;
_setMinimized(false);
return;
}
// 3. First sample after attaching — establish the baseline, decide
// nothing. This is what stops a restored scroll offset from minimizing
// the bar before the user has touched anything.
if (previous == null) return;
final delta = sample.pixels - previous;
// 4. Not a user scroll at all: jumpTo, a PageStorage restore, the keyboard
// changing the viewport inset, or a layout correction. None of these
// begin a scrolling activity, so the direction is still idle — whereas
// a real drag, and the entire momentum phase after it, is not.
if (sample.direction == ScrollDirection.idle) {
_accumulated = 0.0;
return;
}
// 5. A teleport rather than a scroll. Covers animateTo, which keeps a
// stale non-idle direction, and single-frame content-dimension jumps.
if (delta.abs() > sample.viewportDimension * 0.5) {
_accumulated = 0.0;
return;
}
// 6. Rubber-band at the far edge: the offset keeps moving with no intent
// behind it. This must sit after the idle check and before accumulation
// — during a bounce the delta reverses sign while the direction is
// still the direction of the fling, which would otherwise read as a
// scroll back the other way and expand the bar.
if (sample.outOfRange) {
_accumulated = 0.0;
return;
}
// 7. Accumulate since the last reversal.
final effective = _effectiveDelta(delta);
if (effective == 0.0) return;
if (effective.sign != _accumulated.sign) _accumulated = 0.0;
_accumulated += effective;
if (_accumulated >= kMinimizeThreshold) {
_accumulated = 0.0;
_setMinimized(true);
} else if (_accumulated <= -kExpandThreshold) {
_accumulated = 0.0;
_setMinimized(false);
}
}