Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 108 additions & 26 deletions packages/flutter/lib/src/material/scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand All @@ -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
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -1479,19 +1560,22 @@ class ScaffoldState extends State<Scaffold> 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),
),
);
}
Expand Down Expand Up @@ -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) {
Expand All @@ -1606,16 +1690,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
}

if (_snackBars.isNotEmpty) {
final bool removeBottomPadding = widget.persistentFooterButtons != null ||
widget.bottomNavigationBar != null;
_addIfNonNull(
children,
_snackBars.first._widget,
_ScaffoldSlot.snackBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: removeBottomPadding,
removeBottomPadding: widget.bottomNavigationBar != null || widget.persistentFooterButtons != null,
);
}

Expand Down Expand Up @@ -1676,7 +1758,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: widget.resizeToAvoidBottomPadding,
removeBottomPadding: _resizeToAvoidBottomInset,
);
}

Expand Down Expand Up @@ -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(
Expand Down
49 changes: 39 additions & 10 deletions packages/flutter/lib/src/widgets/media_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for adding this explanation.

/// [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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a "see also" section that links to the explanation given on the top level of MediaQueryData that illustrates the difference between the two?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, done.

///
/// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the doc for padding somehow also mention its independence from viewInsets and how the two relate (even if it's only in a "see also" bullet)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a see also bullet per the previous comment.

Expand All @@ -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;
Expand Down
Loading