Skip to content

Commit 4a7e30f

Browse files
Add DisplayFeatureSubScreen widget (#92907)
1 parent ba4d63a commit 4a7e30f

5 files changed

Lines changed: 680 additions & 0 deletions

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:math' as math;
6+
import 'dart:ui' show DisplayFeature;
7+
import 'package:flutter/foundation.dart';
8+
import 'package:flutter/gestures.dart';
9+
import 'package:flutter/rendering.dart';
10+
11+
import 'basic.dart';
12+
import 'debug.dart';
13+
import 'framework.dart';
14+
import 'media_query.dart';
15+
16+
/// Positions [child] such that it avoids overlapping any [DisplayFeature] that
17+
/// splits the screen into sub-screens.
18+
///
19+
/// A [DisplayFeature] splits the screen into sub-screens when both these
20+
/// conditions are met:
21+
///
22+
/// * it obstructs the screen, meaning the area it occupies is not 0. Display
23+
/// features of type [DisplayFeatureType.fold] can have height 0 or width 0
24+
/// and not be obstructing the screen.
25+
/// * it is at least as tall as the screen, producing a left and right
26+
/// sub-screen or it is at least as wide as the screen, producing a top and
27+
/// bottom sub-screen
28+
///
29+
/// After determining the sub-screens, the closest one to [anchorPoint] is used
30+
/// to render the [child].
31+
///
32+
/// If no [anchorPoint] is provided, then [Directionality] is used:
33+
///
34+
/// * for [TextDirection.ltr], [anchorPoint] is `Offset.zero`, which will
35+
/// cause the [child] to appear in the top-left sub-screen.
36+
/// * for [TextDirection.rtl], [anchorPoint] is `Offset(double.maxFinite, 0)`,
37+
/// which will cause the [child] to appear in the top-right sub-screen.
38+
///
39+
/// If no [anchorPoint] is provided, and there is no [Directionality] ancestor
40+
/// widget in the tree, then the widget asserts during build in debug mode.
41+
///
42+
/// Similarly to [SafeArea], this widget assumes there is no added padding
43+
/// between it and the first [MediaQuery] ancestor. The [child] is wrapped in a
44+
/// new [MediaQuery] instance containing the [DisplayFeature]s that exist in the
45+
/// selected sub-screen, with coordinates relative to the sub-screen. Padding is
46+
/// also adjusted to zero out any sides that were avoided by this widget.
47+
///
48+
/// See also:
49+
///
50+
/// * [showDialog], which is a way to display a [DialogRoute].
51+
/// * [showCupertinoDialog], which displays an iOS-style dialog.
52+
class DisplayFeatureSubScreen extends StatelessWidget {
53+
/// Creates a widget that positions its child so that it avoids display
54+
/// features.
55+
const DisplayFeatureSubScreen({
56+
Key? key,
57+
this.anchorPoint,
58+
required this.child,
59+
}) : super(key: key);
60+
61+
/// The anchor point used to pick the closest sub-screen.
62+
///
63+
/// If the anchor point sits inside one of these sub-screens, then that
64+
/// sub-screen is picked. If not, then the sub-screen with the closest edge to
65+
/// the point is used.
66+
///
67+
/// [Offset.zero] is the top-left corner of the available screen space. For a
68+
/// vertically split dual-screen device, this is the top-left corner of the
69+
/// left screen.
70+
///
71+
/// When this is null, [Directionality] is used:
72+
///
73+
/// * for [TextDirection.ltr], [anchorPoint] is [Offset.zero], which will
74+
/// cause the top-left sub-screen to be picked.
75+
/// * for [TextDirection.rtl], [anchorPoint] is
76+
/// `Offset(double.maxFinite, 0)`, which will cause the top-right
77+
/// sub-screen to be picked.
78+
final Offset? anchorPoint;
79+
80+
/// The widget below this widget in the tree.
81+
///
82+
/// The padding on the [MediaQuery] for the [child] will be suitably adjusted
83+
/// to zero out any sides that were avoided by this widget. The [MediaQuery]
84+
/// for the [child] will no longer contain any display features that split the
85+
/// screen into sub-screens.
86+
///
87+
/// {@macro flutter.widgets.ProxyWidget.child}
88+
final Widget child;
89+
90+
@override
91+
Widget build(BuildContext context) {
92+
assert(anchorPoint != null || debugCheckHasDirectionality(
93+
context,
94+
why: 'to determine which sub-screen DisplayFeatureSubScreen uses',
95+
alternative: "Alternatively, consider specifying the 'anchorPoint' argument on the DisplayFeatureSubScreen.",
96+
));
97+
final MediaQueryData mediaQuery = MediaQuery.of(context);
98+
final Size parentSize = mediaQuery.size;
99+
final Rect wantedBounds = Offset.zero & parentSize;
100+
final Offset resolvedAnchorPoint = _capOffset(anchorPoint ?? _fallbackAnchorPoint(context), parentSize);
101+
final Iterable<Rect> subScreens = _subScreensInBounds(wantedBounds, _avoidBounds(mediaQuery));
102+
final Rect closestSubScreen = _closestToAnchorPoint(subScreens, resolvedAnchorPoint);
103+
104+
return Padding(
105+
padding: EdgeInsets.only(
106+
left: closestSubScreen.left,
107+
top: closestSubScreen.top,
108+
right: parentSize.width - closestSubScreen.right,
109+
bottom: parentSize.height - closestSubScreen.bottom,
110+
),
111+
child: MediaQuery(
112+
data: mediaQuery.removeDisplayFeatures(closestSubScreen),
113+
child: child,
114+
),
115+
);
116+
}
117+
118+
static Offset _fallbackAnchorPoint(BuildContext context) {
119+
final TextDirection textDirection = Directionality.of(context);
120+
switch (textDirection) {
121+
case TextDirection.rtl:
122+
return const Offset(double.maxFinite, 0);
123+
case TextDirection.ltr:
124+
return Offset.zero;
125+
}
126+
}
127+
128+
static Iterable<Rect> _avoidBounds(MediaQueryData mediaQuery) {
129+
return mediaQuery.displayFeatures.map((DisplayFeature d) => d.bounds)
130+
.where((Rect r) => r.shortestSide > 0);
131+
}
132+
133+
/// Returns the closest sub-screen to the [anchorPoint].
134+
static Rect _closestToAnchorPoint(Iterable<Rect> subScreens, Offset anchorPoint) {
135+
Rect closestScreen = subScreens.first;
136+
double closestDistance = _distanceFromPointToRect(anchorPoint, closestScreen);
137+
for (final Rect screen in subScreens) {
138+
final double subScreenDistance = _distanceFromPointToRect(anchorPoint, screen);
139+
if (subScreenDistance < closestDistance) {
140+
closestScreen = screen;
141+
closestDistance = subScreenDistance;
142+
}
143+
}
144+
return closestScreen;
145+
}
146+
147+
static double _distanceFromPointToRect(Offset point, Rect rect) {
148+
// Cases for point position relative to rect:
149+
// 1 2 3
150+
// 4 [R] 5
151+
// 6 7 8
152+
if (point.dx < rect.left) {
153+
if (point.dy < rect.top) {
154+
// Case 1
155+
return (point - rect.topLeft).distance;
156+
} else if (point.dy > rect.bottom) {
157+
// Case 6
158+
return (point - rect.bottomLeft).distance;
159+
} else {
160+
// Case 4
161+
return rect.left - point.dx;
162+
}
163+
} else if (point.dx > rect.right) {
164+
if (point.dy < rect.top) {
165+
// Case 3
166+
return (point - rect.topRight).distance;
167+
} else if (point.dy > rect.bottom) {
168+
// Case 8
169+
return (point - rect.bottomRight).distance;
170+
} else {
171+
// Case 5
172+
return point.dx - rect.right;
173+
}
174+
} else {
175+
if (point.dy < rect.top) {
176+
// Case 2
177+
return rect.top - point.dy;
178+
} else if (point.dy > rect.bottom) {
179+
// Case 7
180+
return point.dy - rect.bottom;
181+
} else {
182+
// Case R
183+
return 0;
184+
}
185+
}
186+
}
187+
188+
/// Returns sub-screens resulted by dividing [wantedBounds] along items of
189+
/// [avoidBounds] that are at least as high or as wide.
190+
static Iterable<Rect> _subScreensInBounds(Rect wantedBounds, Iterable<Rect> avoidBounds) {
191+
Iterable<Rect> subScreens = <Rect>[wantedBounds];
192+
for (final Rect bounds in avoidBounds) {
193+
final List<Rect> newSubScreens = <Rect>[];
194+
for (final Rect screen in subScreens) {
195+
if (screen.top >= bounds.top && screen.bottom <= bounds.bottom) {
196+
// Display feature splits the screen vertically
197+
if (screen.left < bounds.left) {
198+
// There is a smaller sub-screen, left of the display feature
199+
newSubScreens.add(Rect.fromLTWH(
200+
screen.left,
201+
screen.top,
202+
bounds.left - screen.left,
203+
screen.height,
204+
));
205+
}
206+
if (screen.right > bounds.right) {
207+
// There is a smaller sub-screen, right of the display feature
208+
newSubScreens.add(Rect.fromLTWH(
209+
bounds.right,
210+
screen.top,
211+
screen.right - bounds.right,
212+
screen.height,
213+
));
214+
}
215+
} else if (screen.left >= bounds.left && screen.right <= bounds.right) {
216+
// Display feature splits the sub-screen horizontally
217+
if (screen.top < bounds.top) {
218+
// There is a smaller sub-screen, above the display feature
219+
newSubScreens.add(Rect.fromLTWH(
220+
screen.left,
221+
screen.top,
222+
screen.width,
223+
bounds.top - screen.top,
224+
));
225+
}
226+
if (screen.bottom > bounds.bottom) {
227+
// There is a smaller sub-screen, below the display feature
228+
newSubScreens.add(Rect.fromLTWH(
229+
screen.left,
230+
bounds.bottom,
231+
screen.width,
232+
screen.bottom - bounds.bottom,
233+
));
234+
}
235+
} else {
236+
newSubScreens.add(screen);
237+
}
238+
}
239+
subScreens = newSubScreens;
240+
}
241+
return subScreens;
242+
}
243+
244+
static Offset _capOffset(Offset offset, Size maximum) {
245+
if (offset.dx >= 0 && offset.dx <= maximum.width
246+
&& offset.dy >=0 && offset.dy <= maximum.height) {
247+
return offset;
248+
} else {
249+
return Offset(
250+
math.min(math.max(0, offset.dx), maximum.width),
251+
math.min(math.max(0, offset.dy), maximum.height),
252+
);
253+
}
254+
}
255+
}

packages/flutter/lib/src/widgets/media_query.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,55 @@ class MediaQueryData {
567567
);
568568
}
569569

570+
/// Creates a copy of this media query data by removing [displayFeatures] that
571+
/// are completely outside the given sub-screen and adjusting the [padding],
572+
/// [viewInsets] and [viewPadding] to be zero on the sides that are not
573+
/// included in the sub-screen.
574+
///
575+
/// Returns unmodified [MediaQueryData] if the sub-screen coincides with the
576+
/// available screen space.
577+
///
578+
/// Asserts in debug mode, if the given sub-screen is outside the available
579+
/// screen space.
580+
///
581+
/// See also:
582+
///
583+
/// * [DisplayFeatureSubScreen], which removes the display features that
584+
/// split the screen, from the [MediaQuery] and adds a [Padding] widget to
585+
/// position the child to match the selected sub-screen.
586+
MediaQueryData removeDisplayFeatures(Rect subScreen) {
587+
assert(subScreen.left >= 0.0 && subScreen.top >= 0.0 &&
588+
subScreen.right <= size.width && subScreen.bottom <= size.height,
589+
"'subScreen' argument cannot be outside the bounds of the screen");
590+
if (subScreen.size == size && subScreen.topLeft == Offset.zero)
591+
return this;
592+
final double rightInset = size.width - subScreen.right;
593+
final double bottomInset = size.height - subScreen.bottom;
594+
return copyWith(
595+
padding: EdgeInsets.only(
596+
left: math.max(0.0, padding.left - subScreen.left),
597+
top: math.max(0.0, padding.top - subScreen.top),
598+
right: math.max(0.0, padding.right - rightInset),
599+
bottom: math.max(0.0, padding.bottom - bottomInset),
600+
),
601+
viewPadding: EdgeInsets.only(
602+
left: math.max(0.0, viewPadding.left - subScreen.left),
603+
top: math.max(0.0, viewPadding.top - subScreen.top),
604+
right: math.max(0.0, viewPadding.right - rightInset),
605+
bottom: math.max(0.0, viewPadding.bottom - bottomInset),
606+
),
607+
viewInsets: EdgeInsets.only(
608+
left: math.max(0.0, viewInsets.left - subScreen.left),
609+
top: math.max(0.0, viewInsets.top - subScreen.top),
610+
right: math.max(0.0, viewInsets.right - rightInset),
611+
bottom: math.max(0.0, viewInsets.bottom - bottomInset),
612+
),
613+
displayFeatures: displayFeatures.where(
614+
(ui.DisplayFeature displayFeature) => subScreen.overlaps(displayFeature.bounds)
615+
).toList(),
616+
);
617+
}
618+
570619
@override
571620
bool operator ==(Object other) {
572621
if (other.runtimeType != runtimeType)

packages/flutter/lib/widgets.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export 'src/widgets/debug.dart';
3636
export 'src/widgets/default_text_editing_shortcuts.dart';
3737
export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart';
3838
export 'src/widgets/dismissible.dart';
39+
export 'src/widgets/display_feature_sub_screen.dart';
3940
export 'src/widgets/disposable_build_context.dart';
4041
export 'src/widgets/drag_target.dart';
4142
export 'src/widgets/draggable_scrollable_sheet.dart';

0 commit comments

Comments
 (0)