| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| import 'dart:ui' show DisplayFeature, DisplayFeatureState; |
| |
| import 'basic.dart'; |
| import 'debug.dart'; |
| import 'framework.dart'; |
| import 'media_query.dart'; |
| |
| /// Positions [child] such that it avoids overlapping any [DisplayFeature] that |
| /// splits the screen into sub-screens. |
| /// |
| /// A [DisplayFeature] splits the screen into sub-screens when both these |
| /// conditions are met: |
| /// |
| /// * it obstructs the screen, meaning the area it occupies is not 0 or the |
| /// `state` is [DisplayFeatureState.postureHalfOpened]. |
| /// * it is at least as tall as the screen, producing a left and right |
| /// sub-screen or it is at least as wide as the screen, producing a top and |
| /// bottom sub-screen |
| /// |
| /// After determining the sub-screens, the closest one to [anchorPoint] is used |
| /// to render the content. |
| /// |
| /// If no [anchorPoint] is provided, then [Directionality] is used: |
| /// |
| /// * for [TextDirection.ltr], [anchorPoint] is `Offset.zero`, which will |
| /// cause the content to appear in the top-left sub-screen. |
| /// * for [TextDirection.rtl], [anchorPoint] is `Offset(double.maxFinite, 0)`, |
| /// which will cause the content to appear in the top-right sub-screen. |
| /// |
| /// If no [anchorPoint] is provided, and there is no [Directionality] ancestor |
| /// widget in the tree, then the widget asserts during build in debug mode. |
| /// |
| /// Similarly to [SafeArea], this widget assumes there is no added padding |
| /// between it and the first [MediaQuery] ancestor. The [child] is wrapped in a |
| /// new [MediaQuery] instance containing the [DisplayFeature]s that exist in the |
| /// selected sub-screen, with coordinates relative to the sub-screen. Padding is |
| /// also adjusted to zero out any sides that were avoided by this widget. |
| /// |
| /// See also: |
| /// |
| /// * [showDialog], which is a way to display a [DialogRoute]. |
| /// * [showCupertinoDialog], which displays an iOS-style dialog. |
| class DisplayFeatureSubScreen extends StatelessWidget { |
| /// Creates a widget that positions its child so that it avoids display |
| /// features. |
| const DisplayFeatureSubScreen({ |
| super.key, |
| this.anchorPoint, |
| required this.child, |
| }); |
| |
| /// {@template flutter.widgets.DisplayFeatureSubScreen.anchorPoint} |
| /// The anchor point used to pick the closest sub-screen. |
| /// |
| /// If the anchor point sits inside one of these sub-screens, then that |
| /// sub-screen is picked. If not, then the sub-screen with the closest edge to |
| /// the point is used. |
| /// |
| /// [Offset.zero] is the top-left corner of the available screen space. For a |
| /// vertically split dual-screen device, this is the top-left corner of the |
| /// left screen. |
| /// |
| /// When this is null, [Directionality] is used: |
| /// |
| /// * for [TextDirection.ltr], [anchorPoint] is [Offset.zero], which will |
| /// cause the top-left sub-screen to be picked. |
| /// * for [TextDirection.rtl], [anchorPoint] is |
| /// `Offset(double.maxFinite, 0)`, which will cause the top-right |
| /// sub-screen to be picked. |
| /// {@endtemplate} |
| final Offset? anchorPoint; |
| |
| /// The widget below this widget in the tree. |
| /// |
| /// The padding on the [MediaQuery] for the [child] will be suitably adjusted |
| /// to zero out any sides that were avoided by this widget. The [MediaQuery] |
| /// for the [child] will no longer contain any display features that split the |
| /// screen into sub-screens. |
| /// |
| /// {@macro flutter.widgets.ProxyWidget.child} |
| final Widget child; |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(anchorPoint != null || debugCheckHasDirectionality( |
| context, |
| why: 'to determine which sub-screen DisplayFeatureSubScreen uses', |
| alternative: "Alternatively, consider specifying the 'anchorPoint' argument on the DisplayFeatureSubScreen.", |
| )); |
| final MediaQueryData mediaQuery = MediaQuery.of(context); |
| final Size parentSize = mediaQuery.size; |
| final Rect wantedBounds = Offset.zero & parentSize; |
| final Offset resolvedAnchorPoint = _capOffset(anchorPoint ?? _fallbackAnchorPoint(context), parentSize); |
| final Iterable<Rect> subScreens = subScreensInBounds(wantedBounds, avoidBounds(mediaQuery)); |
| final Rect closestSubScreen = _closestToAnchorPoint(subScreens, resolvedAnchorPoint); |
| |
| return Padding( |
| padding: EdgeInsets.only( |
| left: closestSubScreen.left, |
| top: closestSubScreen.top, |
| right: parentSize.width - closestSubScreen.right, |
| bottom: parentSize.height - closestSubScreen.bottom, |
| ), |
| child: MediaQuery( |
| data: mediaQuery.removeDisplayFeatures(closestSubScreen), |
| child: child, |
| ), |
| ); |
| } |
| |
| static Offset _fallbackAnchorPoint(BuildContext context) { |
| final TextDirection textDirection = Directionality.of(context); |
| switch (textDirection) { |
| case TextDirection.rtl: |
| return const Offset(double.maxFinite, 0); |
| case TextDirection.ltr: |
| return Offset.zero; |
| } |
| } |
| |
| /// Returns the areas of the screen that are obstructed by display features. |
| /// |
| /// A [DisplayFeature] obstructs the screen when the the area it occupies is |
| /// not 0 or the `state` is [DisplayFeatureState.postureHalfOpened]. |
| static Iterable<Rect> avoidBounds(MediaQueryData mediaQuery) { |
| return mediaQuery.displayFeatures |
| .where((DisplayFeature d) => d.bounds.shortestSide > 0 || |
| d.state == DisplayFeatureState.postureHalfOpened) |
| .map((DisplayFeature d) => d.bounds); |
| } |
| |
| /// Returns the closest sub-screen to the [anchorPoint]. |
| static Rect _closestToAnchorPoint(Iterable<Rect> subScreens, Offset anchorPoint) { |
| Rect closestScreen = subScreens.first; |
| double closestDistance = _distanceFromPointToRect(anchorPoint, closestScreen); |
| for (final Rect screen in subScreens) { |
| final double subScreenDistance = _distanceFromPointToRect(anchorPoint, screen); |
| if (subScreenDistance < closestDistance) { |
| closestScreen = screen; |
| closestDistance = subScreenDistance; |
| } |
| } |
| return closestScreen; |
| } |
| |
| static double _distanceFromPointToRect(Offset point, Rect rect) { |
| // Cases for point position relative to rect: |
| // 1 2 3 |
| // 4 [R] 5 |
| // 6 7 8 |
| if (point.dx < rect.left) { |
| if (point.dy < rect.top) { |
| // Case 1 |
| return (point - rect.topLeft).distance; |
| } else if (point.dy > rect.bottom) { |
| // Case 6 |
| return (point - rect.bottomLeft).distance; |
| } else { |
| // Case 4 |
| return rect.left - point.dx; |
| } |
| } else if (point.dx > rect.right) { |
| if (point.dy < rect.top) { |
| // Case 3 |
| return (point - rect.topRight).distance; |
| } else if (point.dy > rect.bottom) { |
| // Case 8 |
| return (point - rect.bottomRight).distance; |
| } else { |
| // Case 5 |
| return point.dx - rect.right; |
| } |
| } else { |
| if (point.dy < rect.top) { |
| // Case 2 |
| return rect.top - point.dy; |
| } else if (point.dy > rect.bottom) { |
| // Case 7 |
| return point.dy - rect.bottom; |
| } else { |
| // Case R |
| return 0; |
| } |
| } |
| } |
| |
| /// Returns sub-screens resulted by dividing [wantedBounds] along items of |
| /// [avoidBounds] that are at least as tall or as wide. |
| static Iterable<Rect> subScreensInBounds(Rect wantedBounds, Iterable<Rect> avoidBounds) { |
| Iterable<Rect> subScreens = <Rect>[wantedBounds]; |
| for (final Rect bounds in avoidBounds) { |
| final List<Rect> newSubScreens = <Rect>[]; |
| for (final Rect screen in subScreens) { |
| if (screen.top >= bounds.top && screen.bottom <= bounds.bottom) { |
| // Display feature splits the screen vertically |
| if (screen.left < bounds.left) { |
| // There is a smaller sub-screen, left of the display feature |
| newSubScreens.add(Rect.fromLTWH( |
| screen.left, |
| screen.top, |
| bounds.left - screen.left, |
| screen.height, |
| )); |
| } |
| if (screen.right > bounds.right) { |
| // There is a smaller sub-screen, right of the display feature |
| newSubScreens.add(Rect.fromLTWH( |
| bounds.right, |
| screen.top, |
| screen.right - bounds.right, |
| screen.height, |
| )); |
| } |
| } else if (screen.left >= bounds.left && screen.right <= bounds.right) { |
| // Display feature splits the sub-screen horizontally |
| if (screen.top < bounds.top) { |
| // There is a smaller sub-screen, above the display feature |
| newSubScreens.add(Rect.fromLTWH( |
| screen.left, |
| screen.top, |
| screen.width, |
| bounds.top - screen.top, |
| )); |
| } |
| if (screen.bottom > bounds.bottom) { |
| // There is a smaller sub-screen, below the display feature |
| newSubScreens.add(Rect.fromLTWH( |
| screen.left, |
| bounds.bottom, |
| screen.width, |
| screen.bottom - bounds.bottom, |
| )); |
| } |
| } else { |
| newSubScreens.add(screen); |
| } |
| } |
| subScreens = newSubScreens; |
| } |
| return subScreens; |
| } |
| |
| static Offset _capOffset(Offset offset, Size maximum) { |
| if (offset.dx >= 0 && offset.dx <= maximum.width |
| && offset.dy >=0 && offset.dy <= maximum.height) { |
| return offset; |
| } else { |
| return Offset( |
| math.min(math.max(0, offset.dx), maximum.width), |
| math.min(math.max(0, offset.dy), maximum.height), |
| ); |
| } |
| } |
| } |