Adaptive glass
Glass that reads its own backdrop: a bar over a bright sky turns light, a button over a shadow stays dark, each with a label to match. Off by default and free when off.
GlassAdaptive · GlassBackdropReading · GlassHost · GlassThemeData · Source
Apple's .regular is two materials, dark over dark content and light over light. Without help the package picks one
branch for the whole screen, from what you declared: the host's backdrop and the platform's appearance. A screen is
not one level, though. A bar over a photograph's sky and a button over its shadow sit on different branches of Apple's
own material.
GlassHost(adaptive: GlassAdaptive()) lets each glass look. The host already holds the pixels under every surface, so
it reads back the mean level inside each one's box, and a GlassBar, a GlassCard
or a GlassButton picks the branch of .regular and its label colour from it.
When to use
- Glass over photographs, maps or video, where one declared level is wrong for half the screen.
- Bars and buttons that stay put while the content under them changes from light to dark, such as a full-bleed hero image scrolling under a bar.
- Not over a flat page: declare its colour as the host's
backdropinstead, which is exact and reads nothing.
Usage
MaterialApp(
builder: (BuildContext context, Widget? child) => GlassHost(
adaptive: const GlassAdaptive(),
child: child!,
),
home: const PhotoPage(),
)
That's all. The components under the host follow what is under them.
What a reading changes
- The branch of
.regular. Only when nobody named a finish. A finish named by the component, by an innerGlassThemeor by the host (GlassHost(finish: GlassFinish.regularDark)) is kept. Only its label follows the reading. - The label. The reading stands in for the declared
backdropfor that one glass: the label colour, the high-contrast outline and theminLabelContrastdim are all chosen against it. - Not under
richBackdrop: true. A mean says nothing about the brightest corner of a photograph, so there the label stays chosen against every backdrop, and the reading moves only the branch.
Until a glass has its first reading (its first couple of frames, or a tier that captures nothing), it wears what the declarations give, exactly as with adaptive off.
Behaviour
- No flicker. A reading moves a glass only when it is more than
band(12) code values from the one it last moved on, and not withinhold(600 ms) of its last move. A list of light and dark rows scrolling under a bar keeps the bar on the branch it has. - Moves animate. A glass crossing between branches tweens its tint over
duration(300 ms), and on every frame of it the label is the one that reads on the glass as drawn, so the floor holds halfway too. Under reduced motion it is a cut. - What adapts.
GlassBar,GlassCardandGlassButton. A rawGlassSurface, the scroll edge, the tab bar and the segmented control do not adapt yet.
The demo above turns adaptive on in the site's own host while the page is open, so the site's top bar and side panel
follow it too. The site declares a rich backdrop, so the stage nests a GlassTheme with richBackdrop: false to let
the labels follow as well. It needs the full tier (the High or Ultra setting), the Regular material and the Neutral
tint: a lower tier captures nothing to read, and a named material is kept — and a tint names one, the material with
that colour.
Content on the glass
Inside a component that reads its backdrop, GlassTheme.of(context).reading is what it read, so an icon that is not a
label, or a custom painter, can follow it:
final GlassBackdropReading? reading = GlassTheme.of(context).reading;
final bool overLight = reading?.brightness == Brightness.light;
It is null with adaptive off and before the first reading. level is the mean's luma in code values (0 to 255),
luminance its WCAG relative luminance, and brightness whether black or white stands out more against it. The
branch the glass is on is GlassTheme.of(context).finish.
A custom component does what the built-in ones do with GlassThemeData.adaptedTo(reading). To turn adaptive off for a
subtree, nest GlassTheme(data: GlassTheme.of(context).withAdaptive(null), ...).
Cost
- Off: nothing. No reader exists, nothing is recorded, read back or scheduled.
- On, a still screen: nothing after the first reading. A frame that keeps its capture has nothing new under the glass and reads nothing.
- On, a frame that captures: at most one read-back of a 4 × 4-pixel cell per surface, asynchronously, and at most
once per
interval. The frame that asked doesn't wait for it. - On the web it is dearer. CanvasKit reads back synchronously, a GPU flush on the frame it lands in, so
intervalis a second there rather than 250 ms. Raise it further for a screen that scrolls a lot.
Tip
The finish table of Finishes and the label rules of
Legibility & theme still apply: adaptive only gives each glass its own backdrop.
Code
import 'package:flutter/material.dart';
import 'package:g1455/g1455.dart';
void main() => runApp(const PhotoApp());
class PhotoApp extends StatelessWidget {
const PhotoApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
// One host, reading the backdrop under each glass. No finish named, so
// each bar and button may take the branch of `.regular` it reads.
builder: (BuildContext context, Widget? child) => GlassHost(adaptive: const GlassAdaptive(), child: child!),
home: const PhotoPage(),
);
}
class PhotoPage extends StatelessWidget {
const PhotoPage({super.key});
@override
Widget build(BuildContext context) {
final EdgeInsets safe = MediaQuery.paddingOf(context);
return Scaffold(
body: Stack(
children: <Widget>[
// Bright at the top, dark at the bottom: a sky and its shadow.
const Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[Color(0xFFEAF2FA), Color(0xFFB9D3EA), Color(0xFF1B2A1F), Color(0xFF0A110C)],
stops: <double>[0, 0.5, 0.52, 1],
),
),
),
),
// Over the sky: turns light, with a dark label.
Positioned(
top: safe.top + 8,
left: 16,
right: 16,
child: const GlassBar(child: Text('Lake Tekapo')),
),
// Over the shadow: stays dark, with a white label.
Positioned(
bottom: safe.bottom + 16,
left: 16,
child: GlassButton(onPressed: () {}, child: const Text('Directions')),
),
// Content that follows the reading itself.
Positioned(
bottom: safe.bottom + 16,
right: 16,
child: GlassButton(
onPressed: () {},
semanticLabel: 'Weather',
child: Builder(
builder: (BuildContext context) {
final GlassBackdropReading? reading = GlassTheme.of(context).reading;
return Icon(reading?.brightness == Brightness.light ? Icons.wb_sunny : Icons.nightlight_round);
},
),
),
),
],
),
);
}
}API
GlassHost:
| Parameter | Type | Default | Description |
|---|---|---|---|
adaptive |
GlassAdaptive? |
null |
Turns reading on, and says how. Null: glass goes by what is declared, and nothing is read. |
GlassAdaptive:
| Parameter | Type | Default | Description |
|---|---|---|---|
band |
double |
GlassAdaptive.kDefaultBand (12) |
How far, in code values of luma, a reading must be from the last one to move the glass. Must be ≥ 0. |
hold |
Duration |
GlassAdaptive.kDefaultHold (600 ms) |
The least time between two moves of one glass. |
interval |
Duration |
GlassAdaptive.kDefaultInterval (250 ms; 1 s on the web) |
The least time between two read-backs. |
duration |
Duration |
GlassAdaptive.kDefaultDuration (300 ms) |
How long a glass takes to cross between branches. None under reduced motion. |
GlassBackdropReading
| Member | Type | Description |
|---|---|---|
mean |
Color |
The mean colour of the captured backdrop under the glass, opaque. |
level |
double |
The mean's luma in code values, 0 to 255: the scale .regular switches on. |
luminance |
double |
The mean's WCAG relative luminance. |
brightness |
Brightness |
Light when black stands out more against the mean, dark when white does. Not the branch of the glass. |
GlassThemeData
| Member | Description |
|---|---|
adaptive |
The host's GlassAdaptive, or null. Installed by GlassHost.adaptive. |
reading |
What the glass this theme was installed for read, or null. Read it as GlassTheme.of(context).reading. |
adaptedTo(GlassBackdropReading reading) |
This theme as it applies over one glass that read reading: what a custom component installs around its content. |
withAdaptive(GlassAdaptive? adaptive) |
This theme with adaptive replaced, null included: turns reading off for a subtree. |