diff --git a/lib/stub_ui/lib/src/ui/window.dart b/lib/stub_ui/lib/src/ui/window.dart index 87081b3804fae5e9eab642a44ea9c17a3524f86b..7bcfdb83dc98e61fd661da8e703140ceb28341f8 100644 --- a/lib/stub_ui/lib/src/ui/window.dart +++ b/lib/stub_ui/lib/src/ui/window.dart @@ -85,9 +85,10 @@ enum AppLifecycleState { /// A representation of distances for each of the four edges of a rectangle, /// used to encode the view insets and padding that applications should place -/// around their user interface, as exposed by [Window.viewInsets] and -/// [Window.padding]. View insets and padding are preferably read via -/// [MediaQuery.of]. +/// around their user interface, as exposed by [Window.viewInsets], +/// [Window.viewPadding], and [Window.padding]. View insets and padding are +/// preferably read via [MediaQuery.of], since [MediaQuery] will notify its +/// descendants when these values are updated. /// /// For a generic class that represents distances around a rectangle, see the /// [EdgeInsets] class. @@ -550,12 +551,22 @@ abstract class Window { /// design applications. WindowPadding get viewInsets => WindowPadding.zero; + WindowPadding get viewPadding => WindowPadding.zero; + /// The number of physical pixels on each side of the display rectangle into /// which the application can render, but which may be partially obscured by /// system UI (such as the system notification area), or or physical /// intrusions in the display (e.g. overscan regions on television screens or /// phone sensor housings). /// + /// This value is calculated by taking + /// `max(0.0, Window.viewPadding - Window.viewInsets)`. This will treat a + /// system IME that increases the bottm inset as consuming that much of the + /// bottom padding. For example, on an iPhone X, [Window.padding.bottom] is + /// the same as [Window.viewPadding.bottom] when the soft keyboard is not + /// drawn (to account for the bottom soft button area), but will be `0.0` when + /// the soft keyboard is visible. + /// /// When this changes, [onMetricsChanged] is called. /// /// See also: diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 441341dc5d92f397cfa19b2672d05e38e73466bd..36b5a078140b3bb68b1b81ba49c88a142d03e774 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -21,10 +21,10 @@ dynamic _decodeJSON(String message) { void _updateWindowMetrics(double devicePixelRatio, double width, double height, - double paddingTop, - double paddingRight, - double paddingBottom, - double paddingLeft, + double viewPaddingTop, + double viewPaddingRight, + double viewPaddingBottom, + double viewPaddingLeft, double viewInsetTop, double viewInsetRight, double viewInsetBottom, @@ -32,16 +32,21 @@ void _updateWindowMetrics(double devicePixelRatio, window .._devicePixelRatio = devicePixelRatio .._physicalSize = Size(width, height) - .._padding = WindowPadding._( - top: paddingTop, - right: paddingRight, - bottom: paddingBottom, - left: paddingLeft) + .._viewPadding = WindowPadding._( + top: viewPaddingTop, + right: viewPaddingRight, + bottom: viewPaddingBottom, + left: viewPaddingLeft) .._viewInsets = WindowPadding._( top: viewInsetTop, right: viewInsetRight, bottom: viewInsetBottom, - left: viewInsetLeft); + left: viewInsetLeft) + .._padding = WindowPadding._( + top: math.max(0.0, viewPaddingTop - viewInsetTop), + right: math.max(0.0, viewPaddingRight - viewInsetRight), + bottom: math.max(0.0, viewPaddingBottom - viewInsetBottom), + left: math.max(0.0, viewPaddingLeft - viewInsetLeft)); _invoke(window.onMetricsChanged, window._onMetricsChangedZone); } diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 3c392807d99ca5d84093c9cc9668dd61b2363c7e..2ae620be6895f10bcc01ce78aaee36dff4d3bc59 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -411,6 +411,48 @@ class Locale { /// /// There is a single Window instance in the system, which you can /// obtain from the [window] property. +/// +/// ## Insets and Padding +/// +/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/widgets/window_padding.mp4} +/// +/// In this diagram, the black areas represent system UI that the app cannot +/// draw over. The red area represents view padding that the application may not +/// be able to detect gestures in and may not want to draw in. The grey area +/// represents the system keyboard, which can cover over the bottom view +/// padding when visible. +/// +/// The [Window.viewInsets] are the physical pixels which the operating +/// system reserves for system UI, such as the keyboard, which would fully +/// obscure any content drawn in that area. +/// +/// The [Window.viewPadding] are the physical pixels on each side of the display +/// that may be partially obscured by system UI or by physical intrusions into +/// the display, such as an overscan region on a television or a "notch" on a +/// phone. Unlike the insets, these areas may have portions that show the user +/// application painted pixels without being obscured, such as a notch at the +/// top of a phone that covers only a subset of the area. Insets, on the other +/// hand, either partially or fully obscure the window, such as an opaque +/// keyboard or a partially transluscent statusbar, which cover an area without +/// gaps. +/// +/// The [Window.padding] property is computed from both [Window.viewInsets] and +/// [Window.viewPadding]. It will allow a view inset to consume view padding +/// where appropriate, such as when a phone's keyboard is covering the bottom +/// view padding and so "absorbs" it. +/// +/// Clients that want to position elements relative to the view padding +/// regardless of the view insets should use the [Window.viewPadding] property, +/// e.g. if you wish to draw a widget at the center of the screen with respect +/// to the iPhone "safe area" regardless of whether the keyboard is showing. +/// +/// [Window.padding] is useful for clients that want to know how much padding +/// should be accounted for without concern for the current inset(s) state, e.g. +/// determining whether a gesture should be considered for scrolling purposes. +/// This value varies based on the current state of the insets. For example, a +/// visible keyboard will consume all gestures in the bottom part of the +/// [Window.viewPadding] anyway, so there is no need to account for that in the +/// [Window.padding], which is always safe to use for such calculations. class Window { Window._(); @@ -467,6 +509,10 @@ class Window { /// /// When this changes, [onMetricsChanged] is called. /// + /// The relationship between this [Window.viewInsets], [Window.viewPadding], + /// and [Window.padding] are described in more detail in the documentation for + /// [Window]. + /// /// See also: /// /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to @@ -483,8 +529,47 @@ class Window { /// intrusions in the display (e.g. overscan regions on television screens or /// phone sensor housings). /// + /// Unlike [Window.padding], this value does not change relative to + /// [Window.viewInsets]. For example, on an iPhone X, it will not change in + /// response to the soft keyboard being visible or hidden, whereas + /// [Window.padding] will. + /// /// When this changes, [onMetricsChanged] is called. /// + /// The relationship between this [Window.viewInsets], [Window.viewPadding], + /// and [Window.padding] are described in more detail in the documentation for + /// [Window]. + /// + /// See also: + /// + /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to + /// observe when this value changes. + /// * [MediaQuery.of], a simpler mechanism for the same. + /// * [Scaffold], which automatically applies the padding in material design + /// applications. + WindowPadding get viewPadding => _viewPadding; + WindowPadding _viewPadding = WindowPadding.zero; + + /// The number of physical pixels on each side of the display rectangle into + /// which the application can render, but which may be partially obscured by + /// system UI (such as the system notification area), or or physical + /// intrusions in the display (e.g. overscan regions on television screens or + /// phone sensor housings). + /// + /// This value is calculated by taking + /// `max(0.0, Window.viewPadding - Window.viewInsets)`. This will treat a + /// system IME that increases the bottom inset as consuming that much of the + /// bottom padding. For example, on an iPhone X, [Window.padding.bottom] is + /// the same as [Window.viewPadding.bottom] when the soft keyboard is not + /// drawn (to account for the bottom soft button area), but will be `0.0` when + /// the soft keyboard is visible. + /// + /// When this changes, [onMetricsChanged] is called. + /// + /// The relationship between this [Window.viewInsets], [Window.viewPadding], + /// and [Window.padding] are described in more detail in the documentation for + /// [Window]. + /// /// See also: /// /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 0230321fc6e573611c0d6faa2d93682cebd53242..fc08125a18aee99cdb97d180617a8304a820cb10 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -727,23 +727,14 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) CGFloat scale = [UIScreen mainScreen].scale; // The keyboard is treated as an inset since we want to effectively reduce the window size by the - // keyboard height. We also eliminate any bottom safe-area padding since they keyboard 'consumes' - // the home indicator widget. + // keyboard height. The Dart side will compute a value accounting for the keyboard-consuming + // bottom padding. _viewportMetrics.physical_view_inset_bottom = bottom * scale; - _viewportMetrics.physical_padding_bottom = 0; [self updateViewportMetrics]; } - (void)keyboardWillBeHidden:(NSNotification*)notification { - CGFloat scale = [UIScreen mainScreen].scale; _viewportMetrics.physical_view_inset_bottom = 0; - - // Restore any safe area padding that the keyboard had consumed. - if (@available(iOS 11, *)) { - _viewportMetrics.physical_padding_bottom = self.view.safeAreaInsets.bottom * scale; - } else { - _viewportMetrics.physical_padding_top = [self statusBarPadding] * scale; - } [self updateViewportMetrics]; } diff --git a/testing/dart/window_hooks_integration_test.dart b/testing/dart/window_hooks_integration_test.dart index 7198b42b8d665a230c55d2fa8f8e23be2b9c0868..d0ed135186390d1de014f60bd6af580d48164215 100644 --- a/testing/dart/window_hooks_integration_test.dart +++ b/testing/dart/window_hooks_integration_test.dart @@ -266,5 +266,63 @@ void main() { expect(runZone, same(innerZone)); expect(platformBrightness, equals(Brightness.dark)); }); + + + test('Window padding/insets/viewPadding', () { + final double oldDPR = window.devicePixelRatio; + final Size oldSize = window.physicalSize; + final WindowPadding oldPadding = window.viewPadding; + final WindowPadding oldInsets = window.viewInsets; + + _updateWindowMetrics( + 1.0, // DPR + 800.0, // width + 600.0, // height + 50.0, // padding top + 0.0, // padding right + 40.0, // padding bottom + 0.0, // padding left + 0.0, // inset top + 0.0, // inset right + 0.0, // inset bottom + 0.0, // inset left + ); + + expect(window.viewInsets.bottom, 0.0); + expect(window.viewPadding.bottom, 40.0); + expect(window.padding.bottom, 40.0); + + _updateWindowMetrics( + 1.0, // DPR + 800.0, // width + 600.0, // height + 50.0, // padding top + 0.0, // padding right + 40.0, // padding bottom + 0.0, // padding left + 0.0, // inset top + 0.0, // inset right + 400.0, // inset bottom + 0.0, // inset left + ); + + expect(window.viewInsets.bottom, 400.0); + expect(window.viewPadding.bottom, 40.0); + expect(window.padding.bottom, 0.0); + + _updateWindowMetrics( + oldDPR, // DPR + oldSize.width, // width + oldSize.height, // height + oldPadding.top, // padding top + oldPadding.right, // padding right + oldPadding.bottom, // padding bottom + oldPadding.left, // padding left + oldInsets.top, // inset top + oldInsets.right, // inset right + oldInsets.bottom, // inset bottom + oldInsets.left, // inset left + ); + }); }); }