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