| 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 | |