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';
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<Scaffold> 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<Scaffold> with TickerProviderStateMixin {
@required bool removeTopPadding,
@required bool removeRightPadding,
@required bool removeBottomPadding,
bool removeBottomInset = false,
}) {
if (child != null) {
children.add(
LayoutId(
id: childId,
child: MediaQuery.removePadding(
context: context,
MediaQueryData data = MediaQuery.of(context).removePadding(
removeLeft: removeLeftPadding,
removeTop: removeTopPadding,
removeRight: removeRightPadding,
removeBottom: removeBottomPadding,
child: child,
),
);
if (removeBottomInset)
data = data.removeViewInsets(removeBottom: true);
if (child != null) {
children.add(
LayoutId(
id: childId,
child: MediaQuery(data: data, child: child),
),
);
}
......@@ -1580,8 +1664,8 @@ class ScaffoldState extends State<Scaffold> 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<Scaffold> 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<Scaffold> 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<Scaffold> with TickerProviderStateMixin {
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: widget.resizeToAvoidBottomPadding,
removeBottomPadding: _resizeToAvoidBottomInset,
);
}
......@@ -1722,7 +1804,7 @@ class ScaffoldState extends State<Scaffold> 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(
......
......@@ -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;
......
......@@ -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 {
......
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