From a152a2097c9c6748484059e7e9b0cdf4d857d0fd Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Tue, 15 Jan 2019 08:16:49 -0800 Subject: [PATCH] Deprecate Scaffold resizeToAvoidBottomPadding, now resizeToAvoidBottomInset (#26259) --- .../flutter/lib/src/material/scaffold.dart | 134 ++++++++++++++---- .../flutter/lib/src/widgets/media_query.dart | 49 +++++-- .../flutter/test/material/scaffold_test.dart | 67 ++++++++- 3 files changed, 213 insertions(+), 37 deletions(-) diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 7969a86e07..cb5d8e46b2 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -25,6 +25,13 @@ import 'material.dart'; import 'snack_bar.dart'; import 'theme.dart'; +// Examples can assume: +// TabController tabController +// void setState(VoidCallback fn) { } +// String appBarTitle +// int tabCount +// TickerProvider tickerProvider + const FloatingActionButtonLocation _kDefaultFloatingActionButtonLocation = FloatingActionButtonLocation.endFloat; const FloatingActionButtonAnimator _kDefaultFloatingActionButtonAnimator = FloatingActionButtonAnimator.scaling; @@ -110,14 +117,14 @@ class ScaffoldPrelayoutGeometry { /// and is useful for insetting the [FloatingActionButton] to avoid features like /// the system status bar or the keyboard. /// - /// If [Scaffold.resizeToAvoidBottomPadding] is set to false, [minInsets.bottom] - /// will be 0.0 instead of [MediaQuery.padding.bottom]. + /// If [Scaffold.resizeToAvoidBottomInset] is set to false, [minInsets.bottom] + /// will be 0.0. final EdgeInsets minInsets; /// The [Size] of the whole [Scaffold]. /// /// If the [Size] of the [Scaffold]'s contents is modified by values such as - /// [Scaffold.resizeToAvoidBottomPadding] or the keyboard opening, then the + /// [Scaffold.resizeToAvoidBottomInset] or the keyboard opening, then the /// [scaffoldSize] will not reflect those changes. /// /// This means that [FloatingActionButtonLocation]s designed to reposition @@ -278,7 +285,10 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { @required this.currentFloatingActionButtonLocation, @required this.floatingActionButtonMoveAnimationProgress, @required this.floatingActionButtonMotionAnimator, - }) : assert(previousFloatingActionButtonLocation != null), + }) : assert(minInsets != null), + assert(textDirection != null), + assert(geometryNotifier != null), + assert(previousFloatingActionButtonLocation != null), assert(currentFloatingActionButtonLocation != null); final EdgeInsets minInsets; @@ -694,6 +704,58 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr /// ``` /// {@end-tool} /// +/// ## Scaffold layout, the keyboard, and display "notches" +/// +/// The scaffold will expand to fill the available space. That usually +/// means that it will occupy its entire window or device screen. When +/// the device's keyboard appears the Scaffold's ancestor [MediaQuery] +/// widget's [MediaQueryData.viewInsets] changes and the Scaffold will +/// be rebuilt. By default the scaffold's [body] is resized to make +/// room for the keyboard. To prevent the resize set +/// [resizeToAvoidBottomInset] to false. In either case the focused +/// widget will be scrolled into view if it's within a scrollable +/// container. +/// +/// The [MediaQueryData.padding] value defines areas that might +/// not be completely visible, like the display "notch" on the iPhone +/// X. The scaffold's [body] is not inset by this padding value +/// although an [appBar] or [bottomNavigationBar] will typically +/// cause the body to avoid the padding. The [SafeArea] +/// widget can be used within the scaffold's body to avoid areas +/// like display notches. +/// +/// ## Troubleshooting +/// +/// ### Nested Scaffolds +/// +/// The Scaffold was designed to be the single top level container for +/// a [MaterialApp] and it's typically not necessary to nest +/// scaffolds. For example in a tabbed UI, where the +/// [bottomNavigationBar] is a [TabBar] and the body is a +/// [TabBarView], you might be tempted to make each tab bar view a +/// scaffold with a differently titled AppBar. It would be better to add a +/// listener to the [TabController] that updates the AppBar. +/// +/// ## Sample Code +/// +/// Add a listener to the app's tab controller so that the [AppBar] title of the +/// app's one and only scaffold is reset each time a new tab is selected. +/// +/// ```dart +/// tabController = TabController(vsync: tickerProvider, length: tabCount)..addListener(() { +/// if (!tabController.indexIsChanging) { +/// setState(() { +/// // Rebuild the enclosing scaffold with a new AppBar title +/// appBarTitle = 'Tab ${tabController.index}'; +/// }); +/// } +/// }); +/// ``` +/// +/// Although there are some use cases, like a presentation app that +/// shows embedded flutter content, where nested scaffolds are +/// appropriate, it's best to avoid nesting scaffolds. +/// /// See also: /// /// * [AppBar], which is a horizontal bar typically shown at the top of an app @@ -731,7 +793,8 @@ class Scaffold extends StatefulWidget { this.bottomNavigationBar, this.bottomSheet, this.backgroundColor, - this.resizeToAvoidBottomPadding = true, + this.resizeToAvoidBottomPadding, + this.resizeToAvoidBottomInset, this.primary = true, this.drawerDragStartBehavior = DragStartBehavior.start, }) : assert(primary != null), @@ -743,9 +806,11 @@ class Scaffold extends StatefulWidget { /// The primary content of the scaffold. /// - /// Displayed below the app bar and behind the [floatingActionButton] and - /// [drawer]. To avoid the body being resized to avoid the window padding - /// (e.g., from the onscreen keyboard), see [resizeToAvoidBottomPadding]. + /// Displayed below the [appBar], above the bottom of the ambient + /// [MediaQuery]'s [MediaQueryData.viewInsets], and behind the + /// [floatingActionButton] and [drawer]. If [resizeToAvoidBottomInset] is + /// false then the body is not resized when the onscreen keyboard appears, + /// i.e. it is not inset by `viewInsets.bottom`. /// /// The widget in the body of the scaffold is positioned at the top-left of /// the available space between the app bar and the bottom of the scaffold. To @@ -850,15 +915,25 @@ class Scaffold extends StatefulWidget { /// * [showModalBottomSheet], which displays a modal bottom sheet. final Widget bottomSheet; - /// Whether the [body] (and other floating widgets) should size themselves to - /// avoid the window's bottom padding. + /// This flag is deprecated, please use [resizeToAvoidBottomInset] + /// instead. + /// + /// Originally the name referred [MediaQueryData.padding]. Now it refers + /// [MediaQueryData.viewInsets], so using [resizeToAvoidBottomInset] + /// should be clearer to readers. + @Deprecated('Use resizeToAvoidBottomInset to specify if the body should resize when the keyboard appears') + final bool resizeToAvoidBottomPadding; + + /// If true the [body] and the scaffold's floating widgets should size + /// themselves to avoid the onscreen keyboard whose height is defined by the + /// ambient [MediaQuery]'s [MediaQueryData.viewInsets] `bottom` property. /// /// For example, if there is an onscreen keyboard displayed above the /// scaffold, the body can be resized to avoid overlapping the keyboard, which /// prevents widgets inside the body from being obscured by the keyboard. /// /// Defaults to true. - final bool resizeToAvoidBottomPadding; + final bool resizeToAvoidBottomInset; /// Whether this scaffold is being displayed at the top of the screen. /// @@ -1399,6 +1474,12 @@ class ScaffoldState extends State with TickerProviderStateMixin { _ScaffoldGeometryNotifier _geometryNotifier; + // Backwards compatibility for deprecated resizeToAvoidBottomPadding property + bool get _resizeToAvoidBottomInset { + // ignore: deprecated_member_use + return widget.resizeToAvoidBottomInset ?? widget.resizeToAvoidBottomPadding ?? true; + } + @override void initState() { super.initState(); @@ -1479,19 +1560,22 @@ class ScaffoldState extends State with TickerProviderStateMixin { @required bool removeTopPadding, @required bool removeRightPadding, @required bool removeBottomPadding, + bool removeBottomInset = false, }) { + MediaQueryData data = MediaQuery.of(context).removePadding( + removeLeft: removeLeftPadding, + removeTop: removeTopPadding, + removeRight: removeRightPadding, + removeBottom: removeBottomPadding, + ); + if (removeBottomInset) + data = data.removeViewInsets(removeBottom: true); + if (child != null) { children.add( LayoutId( id: childId, - child: MediaQuery.removePadding( - context: context, - removeLeft: removeLeftPadding, - removeTop: removeTopPadding, - removeRight: removeRightPadding, - removeBottom: removeBottomPadding, - child: child, - ), + child: MediaQuery(data: data, child: child), ), ); } @@ -1580,8 +1664,8 @@ class ScaffoldState extends State with TickerProviderStateMixin { removeLeftPadding: false, removeTopPadding: widget.appBar != null, removeRightPadding: false, - removeBottomPadding: widget.bottomNavigationBar != null || - widget.persistentFooterButtons != null, + removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null, + removeBottomInset: _resizeToAvoidBottomInset, ); if (widget.appBar != null) { @@ -1606,8 +1690,6 @@ class ScaffoldState extends State with TickerProviderStateMixin { } if (_snackBars.isNotEmpty) { - final bool removeBottomPadding = widget.persistentFooterButtons != null || - widget.bottomNavigationBar != null; _addIfNonNull( children, _snackBars.first._widget, @@ -1615,7 +1697,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, - removeBottomPadding: removeBottomPadding, + removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null, ); } @@ -1676,7 +1758,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { removeLeftPadding: false, removeTopPadding: true, removeRightPadding: false, - removeBottomPadding: widget.resizeToAvoidBottomPadding, + removeBottomPadding: _resizeToAvoidBottomInset, ); } @@ -1722,7 +1804,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { // The minimum insets for contents of the Scaffold to keep visible. final EdgeInsets minInsets = mediaQuery.padding.copyWith( - bottom: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0, + bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0, ); return _ScaffoldScope( diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index 46bd617e49..991f5b0d9c 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -30,6 +30,25 @@ enum Orientation { /// If no [MediaQuery] is in scope then the [MediaQuery.of] method will throw an /// exception, unless the `nullOk` argument is set to true, in which case it /// returns null. +/// +/// MediaQueryData includes two [EdgeInsets] values: +/// [padding] and [viewInsets]. These +/// values reflect the configuration of the device and are used by +/// many top level widgets, like [SafeArea] and the Cupertino and +/// Material scaffold widgets. The padding value defines areas that +/// might not be completely visible, like the display "notch" on the +/// iPhone X. The viewInsets value defines areas that aren't visible at +/// all, typically because they're obscured by the device's keyboard. +/// +/// The viewInsets and padding values are independent, they're both +/// measured from the edges of the MediaQuery widget's bounds. The +/// bounds of the top level MediaQuery created by [WidgetsApp] are the +/// same as the window that contains the app. +/// +/// Widgets whose layouts consume space defined by [viewInsets] or +/// [padding] shoud enclose their children in secondary MediaQuery +/// widgets that reduce those properties by the same amount. +/// The [removePadding] and [removeInsets] methods are useful for this. @immutable class MediaQueryData { /// Creates data for a media query with explicit values. @@ -67,7 +86,7 @@ class MediaQueryData { boldText = window.accessibilityFeatures.boldText, alwaysUse24HourFormat = window.alwaysUse24HourFormat; - /// The size of the media in logical pixel (e.g, the size of the screen). + /// The size of the media in logical pixels (e.g, the size of the screen). /// /// Logical pixels are roughly the same visual size across devices. Physical /// pixels are the size of the actual hardware pixels on the device. The @@ -91,17 +110,25 @@ class MediaQueryData { /// textScaleFactor defined for a [BuildContext]. final double textScaleFactor; - /// The number of physical pixels on each side of the display rectangle into - /// which the application can render, but over which the operating system - /// will likely place system UI, such as the keyboard, that fully obscures - /// any content. + /// The parts of the display that are completely obscured by system UI, + /// typically by the device's keyboard. + /// + /// When a mobile device's keyboard is visible `viewInsets.bottom` + /// corresponds to the top of the keyboard. + /// + /// This value is independent of the [padding]: both values are + /// measured from the edges of the [MediaQuery] widget's bounds. The + /// bounds of the top level MediaQuery created by [WidgetsApp] are the + /// same as the window (often the mobile device screen) that contains the app. + /// + /// See also: + /// + /// * [MediaQueryData], which provides some additional detail about this + /// property and how it differs from [padding]. final EdgeInsets viewInsets; - /// 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). + /// The parts of the display that are partially obscured by system UI, + /// typically by the hardware display "notches" or the system status bar. /// /// If you consumed this padding (e.g. by building a widget that envelops or /// accounts for this padding in its layout in such a way that children are @@ -111,6 +138,8 @@ class MediaQueryData { /// /// See also: /// + /// * [MediaQueryData], which provides some additional detail about this + /// property and how it differs from [viewInsets]. /// * [SafeArea], a widget that consumes this padding with a [Padding] widget /// and automatically removes it from the [MediaQuery] for its child. final EdgeInsets padding; diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index d1046014f9..e36f451da0 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -59,7 +59,20 @@ void main() { child: Scaffold( appBar: AppBar(title: const Text('Title')), body: Container(key: bodyKey), - resizeToAvoidBottomPadding: false, + resizeToAvoidBottomInset: false, + ), + ))); + + bodyBox = tester.renderObject(find.byKey(bodyKey)); + expect(bodyBox.size, equals(const Size(800.0, 544.0))); + + // Backwards compatiblity: deprecated resizeToAvoidBottomPadding flag + await tester.pumpWidget(boilerplate(MediaQuery( + data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100.0)), + child: Scaffold( + appBar: AppBar(title: const Text('Title')), + body: Container(key: bodyKey), + resizeToAvoidBottomPadding: false, // ignore: deprecated_member_use ), ))); @@ -1200,6 +1213,58 @@ void main() { expect(scaffoldState.isDrawerOpen, true); }); }); + + testWidgets('Nested scaffold body insets', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/20295 + + final Key bodyKey = UniqueKey(); + + Widget buildFrame(bool innerResizeToAvoidBottomInset, bool outerResizeToAvoidBottomInset) { + return Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100.0)), + child: Builder( + builder: (BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: outerResizeToAvoidBottomInset, + body: Builder( + builder: (BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: innerResizeToAvoidBottomInset, + body: Container(key: bodyKey), + ); + }, + ), + ); + }, + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(true, true)); + expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0)); + + await tester.pumpWidget(buildFrame(false, true)); + expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0)); + + await tester.pumpWidget(buildFrame(true, false)); + expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0)); + + // This is the only case where the body is not bottom inset. + await tester.pumpWidget(buildFrame(false, false)); + expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0)); + + await tester.pumpWidget(buildFrame(null, null)); // resizeToAvoidBottomInset default is true + expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0)); + + await tester.pumpWidget(buildFrame(null, false)); + expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0)); + + await tester.pumpWidget(buildFrame(false, null)); + expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0)); + }); } class _GeometryListener extends StatefulWidget {