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';
9library;
10
11import 'dart:math' as math;
12import 'dart:ui' show DisplayFeature, DisplayFeatureState;
13
14import 'basic.dart';
15import 'debug.dart';
16import 'framework.dart';
17import '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.
54class 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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com