GlassContentAwareScope class
Drives iOS 26-style content-aware light/dark adaptation for glass controls floating over scrolling content.
iOS 26 bars and controls watch the content scrolling underneath them and flip between their light and dark appearance so the glyphs always keep contrast — light controls over bright photos, dark controls over a dark album cover. This scope productizes that behavior:
- Wrap the screen (typically the
Scaffold) in a GlassContentAwareScope. - Wrap the scrolling content — not the bars — in a GlassContentAwareContent. This marks the region that is sampled. The glass controls themselves must stay outside of it so the capture sees the content behind them rather than their own rendering.
- Set
adaptiveBrightness: trueonGlassBottomBar/GlassSearchableBottomBar(or wrap any custom control in a GlassContentAwareBrightness).
GlassContentAwareScope(
child: Scaffold(
extendBody: true, // content scrolls underneath the bar
body: GlassContentAwareContent(
child: ListView(...),
),
bottomNavigationBar: GlassBottomBar(
adaptiveBrightness: true,
...
),
),
)
The same wiring applies with GlassScaffold: wrap the scaffold in the
scope and the scrolling content inside body in a
GlassContentAwareContent. The scroll-edge fade that GlassScaffold
renders sits outside the captured region, so it never feeds back into
the vote.
How the verdict is decided
The scope captures the content boundary once per sample (a heavily
downscaled, asynchronous toImage readback) and serves every registered
control from that single capture. Each control's own rectangle is mapped
into the captured image, divided into a small grid of cells, and each
cell casts a vote: which glyph color reads better over this content —
the light variant's dark glyphs, or the dark variant's light glyphs?
The comparison uses WCAG contrast ratios, so the verdict is a contrast
vote, not a luminance threshold: dark glyphs hold contrast over light
and medium content, which keeps controls in their light appearance
through the medium range and only flips them when the content is
genuinely dark — matching the native behavior and resisting mode-flapping
on mixed content such as photo grids.
On top of the vote, two flap-prevention layers are applied per control:
- Sticky ties — a tied or sub-threshold vote keeps the current appearance.
- Dual-threshold hysteresis — flipping light → dark requires the dark vote fraction to reach lightToDarkThreshold, while flipping back requires the light fraction to reach darkToLightThreshold. Content sitting right at the boundary therefore cannot oscillate.
Sampling cost
Sampling is scroll-aware: a ScrollNotification listener inside the scope starts a periodic sampler (every sampleInterval) when scrolling begins and stops it when scrolling ends, with one trailing sample to capture the settled state. While nothing moves, the sample rate is zero. The capture itself is downscaled to roughly 16 physical pixels per grid cell, so the readback is a few kilobytes regardless of screen size.
Because the automatic triggers are scroll-driven, content that changes without emitting scroll notifications — pan/zoom viewers such as InteractiveViewer, asynchronously decoded images, programmatic restyles — should request a sample explicitly when it settles:
GlassContentAwareScope.maybeOf(context)?.requestSample();
Known limit: PlatformViews
Content rendered by an iOS PlatformView (e.g. a map) cannot be captured
by toImage — the same wall as platformViewBackdrop. For those
screens, drive the appearance explicitly with
GlassBottomBar.brightnessOverride (which bypasses the sampler
entirely) keyed off your own signal, such as the active map style.
See also:
- GlassContentAwareContent, which marks the sampled region.
- GlassContentAwareBrightness, the per-control consumer used by the bars and available to custom controls.
- Inheritance
-
- Object
- DiagnosticableTree
- Widget
- StatefulWidget
- GlassContentAwareScope
Constructors
- GlassContentAwareScope({required Widget child, Duration sampleInterval = const Duration(milliseconds: 180), Duration flipDuration = const Duration(milliseconds: 200), Curve flipCurve = Curves.easeInOut, double lightToDarkThreshold = 0.6, double darkToLightThreshold = 0.6, Color? backgroundColor, Key? key})
-
Creates a content-aware brightness scope.
const
Properties
- backgroundColor → Color?
-
Color substituted for fully transparent pixels in the capture.
final
- child → Widget
-
The subtree containing both the sampled content and the adaptive
controls.
final
- darkToLightThreshold → double
-
Vote fraction required to flip a control from dark back to light.
final
- flipCurve → Curve
-
Curve of the light ⇄ dark cross-fade when a control flips.
final
- flipDuration → Duration
-
Duration of the light ⇄ dark cross-fade when a control flips.
final
- hashCode → int
-
The hash code for this object.
no setterinherited
- key → Key?
-
Controls how one widget replaces another widget in the tree.
finalinherited
- lightToDarkThreshold → double
-
Vote fraction required to flip a control from light to dark.
final
- runtimeType → Type
-
A representation of the runtime type of the object.
no setterinherited
- sampleInterval → Duration
-
Minimum interval between samples while scrolling is active.
final
Methods
-
createElement(
) → StatefulElement -
Creates a StatefulElement to manage this widget's location in the tree.
inherited
-
createState(
) → State< GlassContentAwareScope> -
Creates the mutable state for this widget at a given location in the tree.
override
-
debugDescribeChildren(
) → List< DiagnosticsNode> -
Returns a list of DiagnosticsNode objects describing this node's
children.
inherited
-
debugFillProperties(
DiagnosticPropertiesBuilder properties) → void -
Add additional properties associated with the node.
inherited
-
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
toDiagnosticsNode(
{String? name, DiagnosticsTreeStyle? style}) → DiagnosticsNode -
Returns a debug representation of the object that is used by debugging
tools and by DiagnosticsNode.toStringDeep.
inherited
-
toString(
{DiagnosticLevel minLevel = DiagnosticLevel.info}) → String -
A string representation of this object.
inherited
-
toStringDeep(
{String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 65}) → String -
Returns a string representation of this node and its descendants.
inherited
-
toStringShallow(
{String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String -
Returns a one-line detailed description of the object.
inherited
-
toStringShort(
) → String -
A short, textual description of this widget.
inherited
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited
Static Methods
-
computeVerdict(
{required Uint8List rgba, required int width, required int height, required List< Rect> cellRects, required Brightness current, required double lightToDarkThreshold, required double darkToLightThreshold, required Color background}) → Brightness - Decides a control's Brightness from a raw RGBA capture.
-
maybeOf(
BuildContext context) → GlassContentAwareScopeState? - The GlassContentAwareScopeState from the closest enclosing scope, or null if there is none.