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:

  1. Wrap the screen (typically the Scaffold) in a GlassContentAwareScope.
  2. 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.
  3. Set adaptiveBrightness: true on GlassBottomBar / 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:

Inheritance

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.