resolve static method
- required EdgeInsets viewPadding,
- required Size size,
- required TargetPlatform platform,
- required TextDirection textDirection,
- List<
DisplayFeature> displayFeatures = const <DisplayFeature>[], - GlassVerticalBarEdge? systemEdge,
- GlassVerticalBarCompression compression = GlassVerticalBarCompression.automatic,
The strip the system has reserved, read from the view's insets.
iPhone Duo reports its strip only as a lateral inset, which carries the
strip's full width on its side. Nothing else insets exactly one side: a
regular iPhone in landscape insets both equally ((62, 0, 62, 20) on
iPhone 17), and an iPad neither, so a single lateral inset is required
rather than a particular width.
The side is the system's to choose, so it is never assumed. A full-screen app has the strip on the right, and the status bar moves into it, leaving a zero top inset. In Split View each app has it on its outer edge, the left for the app on the left, and the status bar stays along the top of the window.
viewPadding rather than padding, so the strip stays put while the
keyboard is up, and it has to be read above any SafeArea, which removes
viewPadding along with padding for its subtree. The insets are
physical, so the strip keeps its side under RTL, where it becomes the
GlassVerticalBarEdge.leading edge.
GlassVerticalBarData.top and GlassVerticalBarData.bottom are what
the insets do not carry: where the status cluster and the camera end.
UIKit reports both as occlusion regions, which move — the cluster grows
with live activities and goes with the status bar — and they are read
from displayFeatures: the controls start below the cutouts that
overlap the top half of the strip and end above those in the bottom
half, and keep GlassVerticalBarMetrics.edgeMargin clear of an end with
none. Flutter does not report them on iOS yet (flutter/flutter#193025),
so GlassNavigationShell adds the regions the package reads from UIKit
itself.
Until a region is reported they come from a table keyed on the screen size, measured on the 27.1 simulator: 170pt on the outer display, 120pt on the inner display in landscape, and in outer landscape, which hides the status bar, 82pt clear of the camera — at the top when the strip is on the left, at the bottom when it is on the right.
Where the status bar keeps a top inset, as in Split View, the strip and the title row start GlassVerticalBarMetrics.edgeMargin below it.
compression resolves GlassVerticalBarData.collapsesTabBar; under
GlassVerticalBarCompression.automatic the tab bar collapses in outer
landscape, the one posture too short for it and the chrome together.
Implementation
static GlassVerticalBarData? resolve({
required EdgeInsets viewPadding,
required Size size,
required TargetPlatform platform,
required TextDirection textDirection,
List<DisplayFeature> displayFeatures = const <DisplayFeature>[],
GlassVerticalBarEdge? systemEdge,
GlassVerticalBarCompression compression =
GlassVerticalBarCompression.automatic,
}) {
if (platform != TargetPlatform.iOS) return null;
var left = viewPadding.left > 0;
final right = viewPadding.right > 0;
// Without an inset on one side only, the strip is the app's to keep, on
// the edge UIKit names, if it names one.
final systemInset = left != right;
if (!systemInset) {
if (systemEdge == null) return null;
left = (systemEdge == GlassVerticalBarEdge.leading) ==
(textDirection == TextDirection.ltr);
}
final onStart = left == (textDirection == TextDirection.ltr);
final stripWidth = !systemInset
? GlassVerticalBarMetrics.stripWidth
: left
? viewPadding.left
: viewPadding.right;
final strip = left
? Rect.fromLTWH(0, 0, stripWidth, size.height)
: Rect.fromLTWH(size.width - stripWidth, 0, stripWidth, size.height);
final width = size.width.round();
final height = size.height.round();
// A window that keeps its status bar along the top, as in Split View.
final statusBarOnTop = viewPadding.top > 0;
// Inner display, landscape: the status cluster sits in the strip above
// the controls, shorter than on the outer display.
final innerLandscape = width == 951 && height == 669;
// Landscape on the outer display hides the status bar entirely.
final outerLandscape = !statusBarOnTop && !innerLandscape && width > height;
// The camera's end of the strip in outer landscape.
const cameraClearance = 82.0;
final belowStatusBar = viewPadding.top + GlassVerticalBarMetrics.edgeMargin;
double? top;
double? bottom;
for (final feature in displayFeatures) {
final bounds = feature.bounds;
if (feature.type != DisplayFeatureType.cutout ||
!bounds.overlaps(strip)) {
continue;
}
if (bounds.center.dy < size.height / 2) {
top =
math.max(top ?? GlassVerticalBarMetrics.edgeMargin, bounds.bottom);
} else {
bottom = math.max(bottom ?? GlassVerticalBarMetrics.edgeMargin,
size.height - bounds.top);
}
}
if (statusBarOnTop) {
top = math.max(top ?? 0, belowStatusBar);
} else if (top == null && bottom == null && systemInset) {
// Where the system keeps the strip, its status cluster and camera sit
// at its ends; a strip the app keeps has neither.
top = innerLandscape
? 120.0
: outerLandscape
? (left ? cameraClearance : null)
: 170.0;
bottom = outerLandscape && right ? cameraClearance : null;
}
return GlassVerticalBarData(
edge: onStart
? GlassVerticalBarEdge.leading
: GlassVerticalBarEdge.trailing,
width: stripWidth,
top: top ?? GlassVerticalBarMetrics.edgeMargin,
bottom: bottom ?? GlassVerticalBarMetrics.edgeMargin,
rowTop: belowStatusBar,
systemInset: systemInset,
collapsesTabBar: switch (compression) {
GlassVerticalBarCompression.automatic => outerLandscape,
GlassVerticalBarCompression.prefersBarItems => true,
GlassVerticalBarCompression.prefersTabBar => false,
},
);
}