Unverified Commit a152a209 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Deprecate Scaffold resizeToAvoidBottomPadding, now resizeToAvoidBottomInset (#26259)

parent 3a694a6d
...@@ -25,6 +25,13 @@ import 'material.dart'; ...@@ -25,6 +25,13 @@ import 'material.dart';
import 'snack_bar.dart'; import 'snack_bar.dart';
import 'theme.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 FloatingActionButtonLocation _kDefaultFloatingActionButtonLocation = FloatingActionButtonLocation.endFloat;
const FloatingActionButtonAnimator _kDefaultFloatingActionButtonAnimator = FloatingActionButtonAnimator.scaling; const FloatingActionButtonAnimator _kDefaultFloatingActionButtonAnimator = FloatingActionButtonAnimator.scaling;
...@@ -110,14 +117,14 @@ class ScaffoldPrelayoutGeometry { ...@@ -110,14 +117,14 @@ class ScaffoldPrelayoutGeometry {
/// and is useful for insetting the [FloatingActionButton] to avoid features like /// and is useful for insetting the [FloatingActionButton] to avoid features like
/// the system status bar or the keyboard. /// the system status bar or the keyboard.
/// ///
/// If [Scaffold.resizeToAvoidBottomPadding] is set to false, [minInsets.bottom] /// If [Scaffold.resizeToAvoidBottomInset] is set to false, [minInsets.bottom]
/// will be 0.0 instead of [MediaQuery.padding.bottom]. /// will be 0.0.
final EdgeInsets minInsets; final EdgeInsets minInsets;
/// The [Size] of the whole [Scaffold]. /// The [Size] of the whole [Scaffold].
/// ///
/// If the [Size] of the [Scaffold]'s contents is modified by values such as /// 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. /// [scaffoldSize] will not reflect those changes.
/// ///
/// This means that [FloatingActionButtonLocation]s designed to reposition /// This means that [FloatingActionButtonLocation]s designed to reposition
...@@ -278,7 +285,10 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { ...@@ -278,7 +285,10 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
@required this.currentFloatingActionButtonLocation, @required this.currentFloatingActionButtonLocation,
@required this.floatingActionButtonMoveAnimationProgress, @required this.floatingActionButtonMoveAnimationProgress,
@required this.floatingActionButtonMotionAnimator, @required this.floatingActionButtonMotionAnimator,
}) : assert(previousFloatingActionButtonLocation != null), }) : assert(minInsets != null),
assert(textDirection != null),
assert(geometryNotifier != null),
assert(previousFloatingActionButtonLocation != null),
assert(currentFloatingActionButtonLocation != null); assert(currentFloatingActionButtonLocation != null);
final EdgeInsets minInsets; final EdgeInsets minInsets;
...@@ -694,6 +704,58 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr ...@@ -694,6 +704,58 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
/// ``` /// ```
/// {@end-tool} /// {@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: /// See also:
/// ///
/// * [AppBar], which is a horizontal bar typically shown at the top of an app /// * [AppBar], which is a horizontal bar typically shown at the top of an app
...@@ -731,7 +793,8 @@ class Scaffold extends StatefulWidget { ...@@ -731,7 +793,8 @@ class Scaffold extends StatefulWidget {
this.bottomNavigationBar, this.bottomNavigationBar,
this.bottomSheet, this.bottomSheet,
this.backgroundColor, this.backgroundColor,
this.resizeToAvoidBottomPadding = true, this.resizeToAvoidBottomPadding,
this.resizeToAvoidBottomInset,
this.primary = true, this.primary = true,
this.drawerDragStartBehavior = DragStartBehavior.start, this.drawerDragStartBehavior = DragStartBehavior.start,
}) : assert(primary != null), }) : assert(primary != null),
...@@ -743,9 +806,11 @@ class Scaffold extends StatefulWidget { ...@@ -743,9 +806,11 @@ class Scaffold extends StatefulWidget {
/// The primary content of the scaffold. /// The primary content of the scaffold.
/// ///
/// Displayed below the app bar and behind the [floatingActionButton] and /// Displayed below the [appBar], above the bottom of the ambient
/// [drawer]. To avoid the body being resized to avoid the window padding /// [MediaQuery]'s [MediaQueryData.viewInsets], and behind the
/// (e.g., from the onscreen keyboard), see [resizeToAvoidBottomPadding]. /// [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 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 /// the available space between the app bar and the bottom of the scaffold. To
...@@ -850,15 +915,25 @@ class Scaffold extends StatefulWidget { ...@@ -850,15 +915,25 @@ class Scaffold extends StatefulWidget {
/// * [showModalBottomSheet], which displays a modal bottom sheet. /// * [showModalBottomSheet], which displays a modal bottom sheet.
final Widget bottomSheet; final Widget bottomSheet;
/// Whether the [body] (and other floating widgets) should size themselves to /// This flag is deprecated, please use [resizeToAvoidBottomInset]
/// avoid the window's bottom padding. /// 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 /// For example, if there is an onscreen keyboard displayed above the
/// scaffold, the body can be resized to avoid overlapping the keyboard, which /// scaffold, the body can be resized to avoid overlapping the keyboard, which
/// prevents widgets inside the body from being obscured by the keyboard. /// prevents widgets inside the body from being obscured by the keyboard.
/// ///
/// Defaults to true. /// Defaults to true.
final bool resizeToAvoidBottomPadding; final bool resizeToAvoidBottomInset;
/// Whether this scaffold is being displayed at the top of the screen. /// Whether this scaffold is being displayed at the top of the screen.
/// ///
...@@ -1399,6 +1474,12 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1399,6 +1474,12 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_ScaffoldGeometryNotifier _geometryNotifier; _ScaffoldGeometryNotifier _geometryNotifier;
// Backwards compatibility for deprecated resizeToAvoidBottomPadding property
bool get _resizeToAvoidBottomInset {
// ignore: deprecated_member_use
return widget.resizeToAvoidBottomInset ?? widget.resizeToAvoidBottomPadding ?? true;
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -1479,19 +1560,22 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1479,19 +1560,22 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
@required bool removeTopPadding, @required bool removeTopPadding,
@required bool removeRightPadding, @required bool removeRightPadding,
@required bool removeBottomPadding, @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) { if (child != null) {
children.add( children.add(
LayoutId( LayoutId(
id: childId, id: childId,
child: MediaQuery.removePadding( child: MediaQuery(data: data, child: child),
context: context,
removeLeft: removeLeftPadding,
removeTop: removeTopPadding,
removeRight: removeRightPadding,
removeBottom: removeBottomPadding,
child: child,
),
), ),
); );
} }
...@@ -1580,8 +1664,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1580,8 +1664,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
removeLeftPadding: false, removeLeftPadding: false,
removeTopPadding: widget.appBar != null, removeTopPadding: widget.appBar != null,
removeRightPadding: false, removeRightPadding: false,
removeBottomPadding: widget.bottomNavigationBar != null || removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null,
widget.persistentFooterButtons != null, removeBottomInset: _resizeToAvoidBottomInset,
); );
if (widget.appBar != null) { if (widget.appBar != null) {
...@@ -1606,8 +1690,6 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1606,8 +1690,6 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
} }
if (_snackBars.isNotEmpty) { if (_snackBars.isNotEmpty) {
final bool removeBottomPadding = widget.persistentFooterButtons != null ||
widget.bottomNavigationBar != null;
_addIfNonNull( _addIfNonNull(
children, children,
_snackBars.first._widget, _snackBars.first._widget,
...@@ -1615,7 +1697,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1615,7 +1697,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
removeLeftPadding: false, removeLeftPadding: false,
removeTopPadding: true, removeTopPadding: true,
removeRightPadding: false, removeRightPadding: false,
removeBottomPadding: removeBottomPadding, removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null,
); );
} }
...@@ -1676,7 +1758,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1676,7 +1758,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
removeLeftPadding: false, removeLeftPadding: false,
removeTopPadding: true, removeTopPadding: true,
removeRightPadding: false, removeRightPadding: false,
removeBottomPadding: widget.resizeToAvoidBottomPadding, removeBottomPadding: _resizeToAvoidBottomInset,
); );
} }
...@@ -1722,7 +1804,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -1722,7 +1804,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
// The minimum insets for contents of the Scaffold to keep visible. // The minimum insets for contents of the Scaffold to keep visible.
final EdgeInsets minInsets = mediaQuery.padding.copyWith( final EdgeInsets minInsets = mediaQuery.padding.copyWith(
bottom: widget.resizeToAvoidBottomPadding ? mediaQuery.viewInsets.bottom : 0.0, bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0,
); );
return _ScaffoldScope( return _ScaffoldScope(
......
...@@ -30,6 +30,25 @@ enum Orientation { ...@@ -30,6 +30,25 @@ enum Orientation {
/// If no [MediaQuery] is in scope then the [MediaQuery.of] method will throw an /// 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 /// exception, unless the `nullOk` argument is set to true, in which case it
/// returns null. /// 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 @immutable
class MediaQueryData { class MediaQueryData {
/// Creates data for a media query with explicit values. /// Creates data for a media query with explicit values.
...@@ -67,7 +86,7 @@ class MediaQueryData { ...@@ -67,7 +86,7 @@ class MediaQueryData {
boldText = window.accessibilityFeatures.boldText, boldText = window.accessibilityFeatures.boldText,
alwaysUse24HourFormat = window.alwaysUse24HourFormat; 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 /// Logical pixels are roughly the same visual size across devices. Physical
/// pixels are the size of the actual hardware pixels on the device. The /// pixels are the size of the actual hardware pixels on the device. The
...@@ -91,17 +110,25 @@ class MediaQueryData { ...@@ -91,17 +110,25 @@ class MediaQueryData {
/// textScaleFactor defined for a [BuildContext]. /// textScaleFactor defined for a [BuildContext].
final double textScaleFactor; final double textScaleFactor;
/// The number of physical pixels on each side of the display rectangle into /// The parts of the display that are completely obscured by system UI,
/// which the application can render, but over which the operating system /// typically by the device's keyboard.
/// will likely place system UI, such as the keyboard, that fully obscures ///
/// any content. /// 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; final EdgeInsets viewInsets;
/// The number of physical pixels on each side of the display rectangle into /// The parts of the display that are partially obscured by system UI,
/// which the application can render, but which may be partially obscured by /// typically by the hardware display "notches" or the system status bar.
/// 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).
/// ///
/// If you consumed this padding (e.g. by building a widget that envelops or /// 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 /// accounts for this padding in its layout in such a way that children are
...@@ -111,6 +138,8 @@ class MediaQueryData { ...@@ -111,6 +138,8 @@ class MediaQueryData {
/// ///
/// See also: /// 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 /// * [SafeArea], a widget that consumes this padding with a [Padding] widget
/// and automatically removes it from the [MediaQuery] for its child. /// and automatically removes it from the [MediaQuery] for its child.
final EdgeInsets padding; final EdgeInsets padding;
......
...@@ -59,7 +59,20 @@ void main() { ...@@ -59,7 +59,20 @@ void main() {
child: Scaffold( child: Scaffold(
appBar: AppBar(title: const Text('Title')), appBar: AppBar(title: const Text('Title')),
body: Container(key: bodyKey), 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() { ...@@ -1200,6 +1213,58 @@ void main() {
expect(scaffoldState.isDrawerOpen, true); 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 { class _GeometryListener extends StatefulWidget {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment