|
| 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 | +} |
0 commit comments