mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
SafeArea (#12292)
* SafeArea * AnimatedSafeArea * AppBar test * Apply feedback
This commit is contained in:
parent
9646e1fbad
commit
4c83ea8bef
|
@ -337,11 +337,15 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
|
|||
left: _kNavBarEdgePadding,
|
||||
right: _kNavBarEdgePadding,
|
||||
),
|
||||
child: new NavigationToolbar(
|
||||
leading: styledLeading,
|
||||
middle: animatedStyledMiddle,
|
||||
trailing: styledTrailing,
|
||||
centerMiddle: true,
|
||||
child: new MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: new NavigationToolbar(
|
||||
leading: styledLeading,
|
||||
middle: animatedStyledMiddle,
|
||||
trailing: styledTrailing,
|
||||
centerMiddle: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -54,10 +54,9 @@ class CupertinoPageScaffold extends StatelessWidget {
|
|||
if (topPadding > 0.0) {
|
||||
final EdgeInsets mediaQueryPadding = MediaQuery.of(context).padding;
|
||||
topPadding += mediaQueryPadding.top;
|
||||
childWithMediaQuery = new MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
padding: mediaQueryPadding.copyWith(top: 0.0),
|
||||
),
|
||||
childWithMediaQuery = new MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -441,8 +441,8 @@ class _AppBarState extends State<AppBar> {
|
|||
|
||||
// The padding applies to the toolbar and tabbar, not the flexible space.
|
||||
if (widget.primary) {
|
||||
appBar = new Padding(
|
||||
padding: new EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
appBar = new SafeArea(
|
||||
top: true,
|
||||
child: appBar,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -93,7 +93,11 @@ class DrawerHeader extends StatelessWidget {
|
|||
curve: curve,
|
||||
child: child == null ? null : new DefaultTextStyle(
|
||||
style: theme.textTheme.body2,
|
||||
child: child,
|
||||
child: new MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -37,13 +37,15 @@ enum _ScaffoldSlot {
|
|||
|
||||
class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
||||
_ScaffoldLayout({
|
||||
@required this.padding,
|
||||
@required this.statusBarHeight,
|
||||
@required this.bottomPadding,
|
||||
@required this.endPadding, // for floating action button
|
||||
@required this.textDirection,
|
||||
});
|
||||
|
||||
final EdgeInsets padding;
|
||||
final double statusBarHeight;
|
||||
final double bottomPadding;
|
||||
final double endPadding;
|
||||
final TextDirection textDirection;
|
||||
|
||||
@override
|
||||
|
@ -56,7 +58,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||
// so the app bar's shadow is drawn on top of the body.
|
||||
|
||||
final BoxConstraints fullWidthConstraints = looseConstraints.tighten(width: size.width);
|
||||
final double bottom = math.max(0.0, size.height - padding.bottom);
|
||||
final double bottom = math.max(0.0, size.height - bottomPadding);
|
||||
double contentTop = 0.0;
|
||||
double contentBottom = bottom;
|
||||
|
||||
|
@ -72,7 +74,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||
}
|
||||
|
||||
if (hasChild(_ScaffoldSlot.persistentFooter)) {
|
||||
final double persistentFooterHeight = layoutChild(_ScaffoldSlot.persistentFooter, fullWidthConstraints.copyWith(maxHeight: contentBottom - contentTop)).height;
|
||||
final BoxConstraints footerConstraints = new BoxConstraints(
|
||||
maxWidth: fullWidthConstraints.maxWidth,
|
||||
maxHeight: math.max(0.0, contentBottom - contentTop),
|
||||
);
|
||||
final double persistentFooterHeight = layoutChild(_ScaffoldSlot.persistentFooter, footerConstraints).height;
|
||||
contentBottom -= persistentFooterHeight;
|
||||
positionChild(_ScaffoldSlot.persistentFooter, new Offset(0.0, contentBottom));
|
||||
}
|
||||
|
@ -102,7 +108,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||
Size snackBarSize = Size.zero;
|
||||
|
||||
if (hasChild(_ScaffoldSlot.bottomSheet)) {
|
||||
bottomSheetSize = layoutChild(_ScaffoldSlot.bottomSheet, fullWidthConstraints.copyWith(maxHeight: contentBottom - contentTop));
|
||||
final BoxConstraints bottomSheetConstraints = new BoxConstraints(
|
||||
maxWidth: fullWidthConstraints.maxWidth,
|
||||
maxHeight: math.max(0.0, contentBottom - contentTop),
|
||||
);
|
||||
bottomSheetSize = layoutChild(_ScaffoldSlot.bottomSheet, bottomSheetConstraints);
|
||||
positionChild(_ScaffoldSlot.bottomSheet, new Offset((size.width - bottomSheetSize.width) / 2.0, bottom - bottomSheetSize.height));
|
||||
}
|
||||
|
||||
|
@ -117,10 +127,10 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||
assert(textDirection != null);
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
fabX = _kFloatingActionButtonMargin;
|
||||
fabX = _kFloatingActionButtonMargin + endPadding;
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
fabX = size.width - fabSize.width - _kFloatingActionButtonMargin;
|
||||
fabX = size.width - fabSize.width - _kFloatingActionButtonMargin - endPadding;
|
||||
break;
|
||||
}
|
||||
double fabY = contentBottom - fabSize.height - _kFloatingActionButtonMargin;
|
||||
|
@ -144,8 +154,9 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||
|
||||
@override
|
||||
bool shouldRelayout(_ScaffoldLayout oldDelegate) {
|
||||
return oldDelegate.padding != padding
|
||||
|| oldDelegate.statusBarHeight != statusBarHeight
|
||||
return oldDelegate.statusBarHeight != statusBarHeight
|
||||
|| oldDelegate.bottomPadding != bottomPadding
|
||||
|| oldDelegate.endPadding != endPadding
|
||||
|| oldDelegate.textDirection != textDirection;
|
||||
}
|
||||
}
|
||||
|
@ -336,10 +347,13 @@ class Scaffold extends StatefulWidget {
|
|||
/// A set of buttons that are displayed at the bottom of the scaffold.
|
||||
///
|
||||
/// Typically this is a list of [FlatButton] widgets. These buttons are
|
||||
/// persistently visible, even of the [body] of the scaffold scrolls.
|
||||
/// persistently visible, even if the [body] of the scaffold scrolls.
|
||||
///
|
||||
/// These widgets will be wrapped in a [ButtonBar].
|
||||
///
|
||||
/// The [persistentFooterButtons] are rendered above the
|
||||
/// [bottomNavigationBar] but below the [body].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://material.google.com/components/buttons.html#buttons-persistent-footer-buttons>
|
||||
|
@ -363,6 +377,9 @@ class Scaffold extends StatefulWidget {
|
|||
///
|
||||
/// Snack bars slide from underneath the bottom navigation bar while bottom
|
||||
/// sheets are stacked on top.
|
||||
///
|
||||
/// The [bottomNavigationBar] is rendered below the [persistentFooterButtons]
|
||||
/// and the [body].
|
||||
final Widget bottomNavigationBar;
|
||||
|
||||
/// Whether the [body] (and other floating widgets) should size themselves to
|
||||
|
@ -747,18 +764,36 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void _addIfNonNull(List<LayoutId> children, Widget child, Object childId) {
|
||||
if (child != null)
|
||||
children.add(new LayoutId(child: child, id: childId));
|
||||
void _addIfNonNull(List<LayoutId> children, Widget child, Object childId, {
|
||||
@required bool removeLeftPadding,
|
||||
@required bool removeTopPadding,
|
||||
@required bool removeRightPadding,
|
||||
bool removeBottomPadding, // defaults to widget.resizeToAvoidBottomPadding
|
||||
}) {
|
||||
if (child != null) {
|
||||
children.add(
|
||||
new LayoutId(
|
||||
id: childId,
|
||||
child: new MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeLeft: removeLeftPadding,
|
||||
removeTop: removeTopPadding,
|
||||
removeRight: removeRightPadding,
|
||||
removeBottom: removeBottomPadding ?? widget.resizeToAvoidBottomPadding,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
EdgeInsets padding = MediaQuery.of(context).padding;
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final EdgeInsets padding = MediaQuery.of(context).padding;
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
if (!widget.resizeToAvoidBottomPadding)
|
||||
padding = new EdgeInsets.fromLTRB(padding.left, padding.top, padding.right, 0.0);
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
|
||||
if (_snackBars.isNotEmpty) {
|
||||
final ModalRoute<dynamic> route = ModalRoute.of(context);
|
||||
|
@ -777,7 +812,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||
|
||||
final List<LayoutId> children = <LayoutId>[];
|
||||
|
||||
_addIfNonNull(children, widget.body, _ScaffoldSlot.body);
|
||||
_addIfNonNull(
|
||||
children,
|
||||
widget.body,
|
||||
_ScaffoldSlot.body,
|
||||
removeLeftPadding: false,
|
||||
removeTopPadding: widget.appBar != null,
|
||||
removeRightPadding: false,
|
||||
);
|
||||
|
||||
if (widget.appBar != null) {
|
||||
final double topPadding = widget.primary ? padding.top : 0.0;
|
||||
|
@ -793,16 +835,28 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||
),
|
||||
),
|
||||
_ScaffoldSlot.appBar,
|
||||
removeLeftPadding: false,
|
||||
removeTopPadding: false,
|
||||
removeRightPadding: false,
|
||||
removeBottomPadding: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (_snackBars.isNotEmpty)
|
||||
_addIfNonNull(children, _snackBars.first._widget, _ScaffoldSlot.snackBar);
|
||||
if (_snackBars.isNotEmpty) {
|
||||
_addIfNonNull(
|
||||
children,
|
||||
_snackBars.first._widget,
|
||||
_ScaffoldSlot.snackBar,
|
||||
removeLeftPadding: false,
|
||||
removeTopPadding: true,
|
||||
removeRightPadding: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.persistentFooterButtons != null) {
|
||||
children.add(new LayoutId(
|
||||
id: _ScaffoldSlot.persistentFooter,
|
||||
child: new Container(
|
||||
_addIfNonNull(
|
||||
children,
|
||||
new Container(
|
||||
decoration: new BoxDecoration(
|
||||
border: new Border(
|
||||
top: new BorderSide(
|
||||
|
@ -810,20 +864,30 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||
),
|
||||
),
|
||||
),
|
||||
child: new ButtonTheme.bar(
|
||||
child: new ButtonBar(
|
||||
children: widget.persistentFooterButtons
|
||||
child: new SafeArea(
|
||||
child: new ButtonTheme.bar(
|
||||
child: new ButtonBar(
|
||||
children: widget.persistentFooterButtons
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
_ScaffoldSlot.persistentFooter,
|
||||
removeLeftPadding: false,
|
||||
removeTopPadding: true,
|
||||
removeRightPadding: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.bottomNavigationBar != null) {
|
||||
children.add(new LayoutId(
|
||||
id: _ScaffoldSlot.bottomNavigationBar,
|
||||
child: widget.bottomNavigationBar,
|
||||
));
|
||||
_addIfNonNull(
|
||||
children,
|
||||
widget.bottomNavigationBar,
|
||||
_ScaffoldSlot.bottomNavigationBar,
|
||||
removeLeftPadding: false,
|
||||
removeTopPadding: true,
|
||||
removeRightPadding: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) {
|
||||
|
@ -836,39 +900,73 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||
children: bottomSheets,
|
||||
alignment: FractionalOffset.bottomCenter,
|
||||
);
|
||||
_addIfNonNull(children, stack, _ScaffoldSlot.bottomSheet);
|
||||
_addIfNonNull(
|
||||
children,
|
||||
stack,
|
||||
_ScaffoldSlot.bottomSheet,
|
||||
removeLeftPadding: false,
|
||||
removeTopPadding: true,
|
||||
removeRightPadding: false,
|
||||
);
|
||||
}
|
||||
|
||||
children.add(new LayoutId(
|
||||
id: _ScaffoldSlot.floatingActionButton,
|
||||
child: new _FloatingActionButtonTransition(
|
||||
_addIfNonNull(
|
||||
children,
|
||||
new _FloatingActionButtonTransition(
|
||||
child: widget.floatingActionButton,
|
||||
)
|
||||
));
|
||||
),
|
||||
_ScaffoldSlot.floatingActionButton,
|
||||
removeLeftPadding: true,
|
||||
removeTopPadding: true,
|
||||
removeRightPadding: true,
|
||||
removeBottomPadding: true,
|
||||
);
|
||||
|
||||
if (themeData.platform == TargetPlatform.iOS) {
|
||||
children.add(new LayoutId(
|
||||
id: _ScaffoldSlot.statusBar,
|
||||
child: new GestureDetector(
|
||||
_addIfNonNull(
|
||||
children,
|
||||
new GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _handleStatusBarTap,
|
||||
// iOS accessibility automatically adds scroll-to-top to the clock in the status bar
|
||||
excludeFromSemantics: true,
|
||||
)
|
||||
));
|
||||
),
|
||||
_ScaffoldSlot.statusBar,
|
||||
removeLeftPadding: false,
|
||||
removeTopPadding: true,
|
||||
removeRightPadding: false,
|
||||
removeBottomPadding: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.drawer != null) {
|
||||
assert(hasDrawer);
|
||||
children.add(new LayoutId(
|
||||
id: _ScaffoldSlot.drawer,
|
||||
child: new DrawerController(
|
||||
_addIfNonNull(
|
||||
children,
|
||||
new DrawerController(
|
||||
key: _drawerKey,
|
||||
child: widget.drawer,
|
||||
)
|
||||
));
|
||||
),
|
||||
_ScaffoldSlot.drawer,
|
||||
// remove the side padding from the side we're not touching
|
||||
removeLeftPadding: textDirection == TextDirection.rtl,
|
||||
removeTopPadding: false,
|
||||
removeRightPadding: textDirection == TextDirection.ltr,
|
||||
removeBottomPadding: false,
|
||||
);
|
||||
}
|
||||
|
||||
double endPadding;
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
endPadding = padding.left;
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
endPadding = padding.right;
|
||||
break;
|
||||
}
|
||||
assert(endPadding != null);
|
||||
|
||||
return new _ScaffoldScope(
|
||||
hasDrawer: hasDrawer,
|
||||
child: new PrimaryScrollController(
|
||||
|
@ -878,9 +976,10 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||
child: new CustomMultiChildLayout(
|
||||
children: children,
|
||||
delegate: new _ScaffoldLayout(
|
||||
padding: padding,
|
||||
statusBarHeight: padding.top,
|
||||
textDirection: Directionality.of(context),
|
||||
bottomPadding: widget.resizeToAvoidBottomPadding ? padding.bottom : 0.0,
|
||||
endPadding: endPadding,
|
||||
textDirection: textDirection,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -336,6 +336,11 @@ abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> exten
|
|||
/// For more complex animations, you'll likely want to use a subclass of
|
||||
/// [AnimatedWidget] such as the [DecoratedBoxTransition] or use your own
|
||||
/// [AnimationController].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AnimatedPadding], which is a subset of this widget that only
|
||||
/// supports animating the [padding].
|
||||
class AnimatedContainer extends ImplicitlyAnimatedWidget {
|
||||
/// Creates a container that animates its parameters implicitly.
|
||||
///
|
||||
|
@ -479,6 +484,66 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer>
|
|||
}
|
||||
}
|
||||
|
||||
/// Animated version of [Padding] which automatically transitions the
|
||||
/// indentation over a given duration whenever the given inset changes.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AnimatedContainer], which can transition more values at once.
|
||||
class AnimatedPadding extends ImplicitlyAnimatedWidget {
|
||||
/// Creates a widget that insets its child by a value that animates
|
||||
/// implicitly.
|
||||
///
|
||||
/// The [padding], [curve], and [duration] arguments must not be null.
|
||||
AnimatedPadding({
|
||||
Key key,
|
||||
@required this.padding,
|
||||
this.child,
|
||||
Curve curve: Curves.linear,
|
||||
@required Duration duration,
|
||||
}) : assert(padding != null),
|
||||
assert(padding.isNonNegative),
|
||||
super(key: key, curve: curve, duration: duration);
|
||||
|
||||
/// The amount of space by which to inset the child.
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
_AnimatedPaddingState createState() => new _AnimatedPaddingState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedPaddingState extends AnimatedWidgetBaseState<AnimatedPadding> {
|
||||
EdgeInsetsGeometryTween _padding;
|
||||
|
||||
@override
|
||||
void forEachTween(TweenVisitor<dynamic> visitor) {
|
||||
_padding = visitor(_padding, widget.padding, (dynamic value) => new EdgeInsetsGeometryTween(begin: value));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Padding(
|
||||
padding: _padding.evaluate(animation),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description.add(new DiagnosticsProperty<EdgeInsetsGeometryTween>('padding', _padding, defaultValue: null));
|
||||
}
|
||||
}
|
||||
|
||||
/// Animated version of [Positioned] which automatically transitions the child's
|
||||
/// position over a given duration whenever the given position changes.
|
||||
///
|
||||
|
|
|
@ -98,6 +98,40 @@ class MediaQueryData {
|
|||
);
|
||||
}
|
||||
|
||||
/// Creates a copy of this media query data but with the given paddings
|
||||
/// replaced with zero.
|
||||
///
|
||||
/// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
|
||||
/// must not be null. If all four are false (the default) then this
|
||||
/// [MediaQueryData] is returned unmodified.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [new MediaQuery.removePadding], which uses this method to remove padding
|
||||
/// from the ambient [MediaQuery].
|
||||
/// * [SafeArea], which both removes the padding from the [MediaQuery] and
|
||||
/// adds a [Padding] widget.
|
||||
MediaQueryData removePadding({
|
||||
bool removeLeft: false,
|
||||
bool removeTop: false,
|
||||
bool removeRight: false,
|
||||
bool removeBottom: false,
|
||||
}) {
|
||||
if (!(removeLeft || removeTop || removeRight || removeBottom))
|
||||
return this;
|
||||
return new MediaQueryData(
|
||||
size: size,
|
||||
devicePixelRatio: devicePixelRatio,
|
||||
textScaleFactor: textScaleFactor,
|
||||
padding: padding.copyWith(
|
||||
left: removeLeft ? 0.0 : null,
|
||||
top: removeTop ? 0.0 : null,
|
||||
right: removeRight ? 0.0 : null,
|
||||
bottom: removeBottom ? 0.0 : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType)
|
||||
|
@ -148,6 +182,44 @@ class MediaQuery extends InheritedWidget {
|
|||
assert(data != null),
|
||||
super(key: key, child: child);
|
||||
|
||||
/// Creates a new [MediaQuery] that inherits from the ambient [MediaQuery] from
|
||||
/// the given context, but removes the specified paddings.
|
||||
///
|
||||
/// The [context] argument is required, must not be null, and must have a
|
||||
/// [MediaQuery] in scope.
|
||||
///
|
||||
/// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
|
||||
/// must not be null. If all four are false (the default) then the returned
|
||||
/// [MediaQuery] reuses the ambient [MediaQueryData] unmodified, which is not
|
||||
/// particularly useful.
|
||||
///
|
||||
/// The [child] argument is required and must not be null.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SafeArea], which both removes the padding from the [MediaQuery] and
|
||||
/// adds a [Padding] widget.
|
||||
factory MediaQuery.removePadding({
|
||||
Key key,
|
||||
@required BuildContext context,
|
||||
bool removeLeft: false,
|
||||
bool removeTop: false,
|
||||
bool removeRight: false,
|
||||
bool removeBottom: false,
|
||||
@required Widget child,
|
||||
}) {
|
||||
return new MediaQuery(
|
||||
key: key,
|
||||
data: MediaQuery.of(context).removePadding(
|
||||
removeLeft: removeLeft,
|
||||
removeTop: removeTop,
|
||||
removeRight: removeRight,
|
||||
removeBottom: removeBottom,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
/// Contains information about the current media.
|
||||
///
|
||||
/// For example, the [MediaQueryData.size] property contains the width and
|
||||
|
|
93
packages/flutter/lib/src/widgets/safe_area.dart
Normal file
93
packages/flutter/lib/src/widgets/safe_area.dart
Normal file
|
@ -0,0 +1,93 @@
|
|||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'debug.dart';
|
||||
import 'framework.dart';
|
||||
import 'media_query.dart';
|
||||
|
||||
/// A widget that insets its child by sufficient padding to avoid
|
||||
/// intrusions by the operating system.
|
||||
///
|
||||
/// For example, this will indent the child by enough to avoid the status bar at
|
||||
/// the top of the screen.
|
||||
///
|
||||
/// It will also indent the child by the amount necessary to avoid The Notch on
|
||||
/// the iPhone X, or other similar creative physical features of the display.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Padding], for insetting widgets in general.
|
||||
/// * [MediaQuery], from which the window padding is obtained.
|
||||
/// * [dart:ui.Window.padding], which reports the padding from the operating
|
||||
/// system.
|
||||
class SafeArea extends StatelessWidget {
|
||||
/// Creates a widget that avoids operating system interfaces.
|
||||
///
|
||||
/// The [left], [top], [right], and [bottom] arguments must not be null.
|
||||
const SafeArea({
|
||||
Key key,
|
||||
this.left: true,
|
||||
this.top: true,
|
||||
this.right: true,
|
||||
this.bottom: true,
|
||||
@required this.child,
|
||||
}) : assert(left != null),
|
||||
assert(top != null),
|
||||
assert(right != null),
|
||||
assert(bottom != null),
|
||||
super(key: key);
|
||||
|
||||
/// Whether to avoid system intrusions on the left.
|
||||
final bool left;
|
||||
|
||||
/// Whether to avoid system intrusions at the top of the screen, typically the
|
||||
/// system status bar.
|
||||
final bool top;
|
||||
|
||||
/// Whether to avoid system intrusions on the right.
|
||||
final bool right;
|
||||
|
||||
/// Whether to avoid system intrusions on the bottom side of the screen.
|
||||
final bool bottom;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// The padding on the [MediaQuery] for the [child] will be suitably adjusted
|
||||
/// to zero out any sides that were avoided by this widget.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
final EdgeInsets padding = MediaQuery.of(context).padding;
|
||||
return new Padding(
|
||||
padding: new EdgeInsets.only(
|
||||
left: left ? padding.left : 0.0,
|
||||
top: top ? padding.top : 0.0,
|
||||
right: right ? padding.right : 0.0,
|
||||
bottom: bottom ? padding.bottom : 0.0,
|
||||
),
|
||||
child: new MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeLeft: left,
|
||||
removeTop: top,
|
||||
removeRight: right,
|
||||
removeBottom: bottom,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description.add(new FlagProperty('left', value: left, ifTrue: 'avoid left padding'));
|
||||
description.add(new FlagProperty('top', value: left, ifTrue: 'avoid top padding'));
|
||||
description.add(new FlagProperty('right', value: left, ifTrue: 'avoid right padding'));
|
||||
description.add(new FlagProperty('bottom', value: left, ifTrue: 'avoid bottom padding'));
|
||||
}
|
||||
}
|
|
@ -64,6 +64,7 @@ export 'src/widgets/preferred_size.dart';
|
|||
export 'src/widgets/primary_scroll_controller.dart';
|
||||
export 'src/widgets/raw_keyboard_listener.dart';
|
||||
export 'src/widgets/routes.dart';
|
||||
export 'src/widgets/safe_area.dart';
|
||||
export 'src/widgets/scroll_activity.dart';
|
||||
export 'src/widgets/scroll_configuration.dart';
|
||||
export 'src/widgets/scroll_context.dart';
|
||||
|
|
|
@ -278,7 +278,7 @@ void main() {
|
|||
expect(tester.getTopRight(find.text('X')).dx, 800.0 - 72.0);
|
||||
});
|
||||
|
||||
testWidgets('AppBar centerTitle:false title overflow OK ', (WidgetTester tester) async {
|
||||
testWidgets('AppBar centerTitle:false title overflow OK', (WidgetTester tester) async {
|
||||
// The app bar's title should be constrained to fit within the available space
|
||||
// between the leading and actions widgets.
|
||||
|
||||
|
@ -1085,4 +1085,64 @@ void main() {
|
|||
expect(tester.renderObject<RenderBox>(find.byKey(key)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
|
||||
expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0));
|
||||
});
|
||||
|
||||
testWidgets('AppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async {
|
||||
const MediaQueryData topPadding100 = const MediaQueryData(padding: const EdgeInsets.only(top: 100.0));
|
||||
|
||||
final Key leadingKey = new UniqueKey();
|
||||
final Key titleKey = new UniqueKey();
|
||||
final Key trailingKey = new UniqueKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: new MediaQuery(
|
||||
data: topPadding100,
|
||||
child: new Scaffold(
|
||||
primary: false,
|
||||
appBar: new AppBar(
|
||||
leading: new Placeholder(key: leadingKey),
|
||||
title: new Placeholder(key: titleKey),
|
||||
actions: <Widget>[ new Placeholder(key: trailingKey) ],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getTopLeft(find.byType(AppBar)), const Offset(0.0, 0.0));
|
||||
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(800.0 - 56.0, 100.0));
|
||||
expect(tester.getTopLeft(find.byKey(titleKey)), const Offset(420.0, 100.0));
|
||||
expect(tester.getTopLeft(find.byKey(trailingKey)), const Offset(4.0, 100.0));
|
||||
});
|
||||
|
||||
testWidgets('SliverAppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async {
|
||||
const MediaQueryData topPadding100 = const MediaQueryData(padding: const EdgeInsets.only(top: 100.0));
|
||||
|
||||
final Key leadingKey = new UniqueKey();
|
||||
final Key titleKey = new UniqueKey();
|
||||
final Key trailingKey = new UniqueKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: new MediaQuery(
|
||||
data: topPadding100,
|
||||
child: new CustomScrollView(
|
||||
primary: true,
|
||||
slivers: <Widget>[
|
||||
new SliverAppBar(
|
||||
leading: new Placeholder(key: leadingKey),
|
||||
title: new Placeholder(key: titleKey),
|
||||
actions: <Widget>[ new Placeholder(key: trailingKey) ],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getTopLeft(find.byType(AppBar)), const Offset(0.0, 0.0));
|
||||
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(800.0 - 56.0, 100.0));
|
||||
expect(tester.getTopLeft(find.byKey(titleKey)), const Offset(420.0, 100.0));
|
||||
expect(tester.getTopLeft(find.byKey(trailingKey)), const Offset(4.0, 100.0));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -527,4 +527,100 @@ void main() {
|
|||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Scaffold and extreme window padding', (WidgetTester tester) async {
|
||||
final Key appBar = new UniqueKey();
|
||||
final Key body = new UniqueKey();
|
||||
final Key floatingActionButton = new UniqueKey();
|
||||
final Key persistentFooterButton = new UniqueKey();
|
||||
final Key drawer = new UniqueKey();
|
||||
final Key bottomNavigationBar = new UniqueKey();
|
||||
final Key insideAppBar = new UniqueKey();
|
||||
final Key insideBody = new UniqueKey();
|
||||
final Key insideFloatingActionButton = new UniqueKey();
|
||||
final Key insidePersistentFooterButton = new UniqueKey();
|
||||
final Key insideDrawer = new UniqueKey();
|
||||
final Key insideBottomNavigationBar = new UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: new MediaQuery(
|
||||
data: const MediaQueryData(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20.0,
|
||||
top: 30.0,
|
||||
right: 50.0,
|
||||
bottom: 70.0,
|
||||
),
|
||||
),
|
||||
child: new Scaffold(
|
||||
appBar: new PreferredSize(
|
||||
preferredSize: const Size(11.0, 13.0),
|
||||
child: new Container(
|
||||
key: appBar,
|
||||
child: new SafeArea(
|
||||
child: new Placeholder(key: insideAppBar),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: new Container(
|
||||
key: body,
|
||||
child: new SafeArea(
|
||||
child: new Placeholder(key: insideBody),
|
||||
),
|
||||
),
|
||||
floatingActionButton: new SizedBox(
|
||||
key: floatingActionButton,
|
||||
width: 77.0,
|
||||
height: 77.0,
|
||||
child: new SafeArea(
|
||||
child: new Placeholder(key: insideFloatingActionButton),
|
||||
),
|
||||
),
|
||||
persistentFooterButtons: <Widget>[
|
||||
new SizedBox(
|
||||
key: persistentFooterButton,
|
||||
width: 100.0,
|
||||
height: 90.0,
|
||||
child: new SafeArea(
|
||||
child: new Placeholder(key: insidePersistentFooterButton),
|
||||
),
|
||||
),
|
||||
],
|
||||
drawer: new Container(
|
||||
key: drawer,
|
||||
width: 204.0,
|
||||
child: new SafeArea(
|
||||
child: new Placeholder(key: insideDrawer),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: new SizedBox(
|
||||
key: bottomNavigationBar,
|
||||
height: 55.0,
|
||||
child: new SafeArea(
|
||||
child: new Placeholder(key: insideBottomNavigationBar),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// open drawer
|
||||
await tester.flingFrom(const Offset(795.0, 5.0), const Offset(-200.0, 0.0), 10.0);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(tester.getRect(find.byKey(appBar)), new Rect.fromLTRB(0.0, 0.0, 800.0, 43.0));
|
||||
expect(tester.getRect(find.byKey(body)), new Rect.fromLTRB(0.0, 43.0, 800.0, 368.0));
|
||||
expect(tester.getRect(find.byKey(floatingActionButton)), new Rect.fromLTRB(36.0, 275.0, 113.0, 352.0));
|
||||
expect(tester.getRect(find.byKey(persistentFooterButton)), new Rect.fromLTRB(28.0, 377.0, 128.0, 467.0));
|
||||
expect(tester.getRect(find.byKey(drawer)), new Rect.fromLTRB(596.0, 0.0, 800.0, 600.0));
|
||||
expect(tester.getRect(find.byKey(bottomNavigationBar)), new Rect.fromLTRB(0.0, 475.0, 800.0, 530.0));
|
||||
expect(tester.getRect(find.byKey(insideAppBar)), new Rect.fromLTRB(20.0, 30.0, 750.0, 43.0));
|
||||
expect(tester.getRect(find.byKey(insideBody)), new Rect.fromLTRB(20.0, 43.0, 750.0, 368.0));
|
||||
expect(tester.getRect(find.byKey(insideFloatingActionButton)), new Rect.fromLTRB(36.0, 275.0, 113.0, 352.0));
|
||||
expect(tester.getRect(find.byKey(insidePersistentFooterButton)), new Rect.fromLTRB(28.0, 377.0, 128.0, 467.0));
|
||||
expect(tester.getRect(find.byKey(insideDrawer)), new Rect.fromLTRB(596.0, 30.0, 750.0, 530.0));
|
||||
expect(tester.getRect(find.byKey(insideBottomNavigationBar)), new Rect.fromLTRB(20.0, 475.0, 750.0, 530.0));
|
||||
});
|
||||
}
|
||||
|
|
61
packages/flutter/test/widgets/animated_padding_test.dart
Normal file
61
packages/flutter/test/widgets/animated_padding_test.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('AnimatedPadding.debugFillProperties', (WidgetTester tester) async {
|
||||
final AnimatedPadding padding = new AnimatedPadding(
|
||||
padding: const EdgeInsets.all(7.0),
|
||||
curve: Curves.ease,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
expect(padding, hasOneLineDescription);
|
||||
});
|
||||
|
||||
testWidgets('AnimatedPadding padding visual-to-directional animation', (WidgetTester tester) async {
|
||||
final Key target = new UniqueKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: new AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.only(right: 50.0),
|
||||
child: new SizedBox.expand(key: target),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSize(find.byKey(target)), const Size(750.0, 600.0));
|
||||
expect(tester.getTopRight(find.byKey(target)), const Offset(750.0, 0.0));
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: new AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsetsDirectional.only(start: 100.0),
|
||||
child: new SizedBox.expand(key: target),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSize(find.byKey(target)), const Size(750.0, 600.0));
|
||||
expect(tester.getTopRight(find.byKey(target)), const Offset(750.0, 0.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(tester.getSize(find.byKey(target)), const Size(725.0, 600.0));
|
||||
expect(tester.getTopRight(find.byKey(target)), const Offset(725.0, 0.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(tester.getSize(find.byKey(target)), const Size(700.0, 600.0));
|
||||
expect(tester.getTopRight(find.byKey(target)), const Offset(700.0, 0.0));
|
||||
});
|
||||
}
|
72
packages/flutter/test/widgets/safe_area_test.dart
Normal file
72
packages/flutter/test/widgets/safe_area_test.dart
Normal file
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('SafeArea - basic', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MediaQuery(
|
||||
data: const MediaQueryData(padding: const EdgeInsets.all(20.0)),
|
||||
child: const SafeArea(
|
||||
left: false,
|
||||
child: const Placeholder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(0.0, 20.0));
|
||||
expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0));
|
||||
});
|
||||
|
||||
testWidgets('SafeArea - nested', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MediaQuery(
|
||||
data: const MediaQueryData(padding: const EdgeInsets.all(20.0)),
|
||||
child: const SafeArea(
|
||||
top: false,
|
||||
child: const SafeArea(
|
||||
right: false,
|
||||
child: const Placeholder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 20.0));
|
||||
expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0));
|
||||
});
|
||||
|
||||
testWidgets('SafeArea - changing', (WidgetTester tester) async {
|
||||
final Widget child = const SafeArea(
|
||||
bottom: false,
|
||||
child: const SafeArea(
|
||||
left: false,
|
||||
bottom: false,
|
||||
child: const Placeholder(),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
new MediaQuery(
|
||||
data: const MediaQueryData(padding: const EdgeInsets.all(20.0)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 20.0));
|
||||
expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 600.0));
|
||||
await tester.pumpWidget(
|
||||
new MediaQuery(
|
||||
data: const MediaQueryData(padding: const EdgeInsets.only(
|
||||
left: 100.0,
|
||||
top: 30.0,
|
||||
right: 0.0,
|
||||
bottom: 40.0,
|
||||
)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(100.0, 30.0));
|
||||
expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue