* SafeArea

* AnimatedSafeArea

* AppBar test

* Apply feedback
This commit is contained in:
Ian Hickson 2017-09-28 17:37:25 -07:00 committed by GitHub
parent 9646e1fbad
commit 4c83ea8bef
13 changed files with 687 additions and 61 deletions

View file

@ -337,11 +337,15 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
left: _kNavBarEdgePadding, left: _kNavBarEdgePadding,
right: _kNavBarEdgePadding, right: _kNavBarEdgePadding,
), ),
child: new NavigationToolbar( child: new MediaQuery.removePadding(
leading: styledLeading, context: context,
middle: animatedStyledMiddle, removeTop: true,
trailing: styledTrailing, child: new NavigationToolbar(
centerMiddle: true, leading: styledLeading,
middle: animatedStyledMiddle,
trailing: styledTrailing,
centerMiddle: true,
),
), ),
), ),
), ),

View file

@ -54,10 +54,9 @@ class CupertinoPageScaffold extends StatelessWidget {
if (topPadding > 0.0) { if (topPadding > 0.0) {
final EdgeInsets mediaQueryPadding = MediaQuery.of(context).padding; final EdgeInsets mediaQueryPadding = MediaQuery.of(context).padding;
topPadding += mediaQueryPadding.top; topPadding += mediaQueryPadding.top;
childWithMediaQuery = new MediaQuery( childWithMediaQuery = new MediaQuery.removePadding(
data: MediaQuery.of(context).copyWith( context: context,
padding: mediaQueryPadding.copyWith(top: 0.0), removeTop: true,
),
child: child, child: child,
); );
} }

View file

@ -441,8 +441,8 @@ class _AppBarState extends State<AppBar> {
// The padding applies to the toolbar and tabbar, not the flexible space. // The padding applies to the toolbar and tabbar, not the flexible space.
if (widget.primary) { if (widget.primary) {
appBar = new Padding( appBar = new SafeArea(
padding: new EdgeInsets.only(top: MediaQuery.of(context).padding.top), top: true,
child: appBar, child: appBar,
); );
} }

View file

@ -93,7 +93,11 @@ class DrawerHeader extends StatelessWidget {
curve: curve, curve: curve,
child: child == null ? null : new DefaultTextStyle( child: child == null ? null : new DefaultTextStyle(
style: theme.textTheme.body2, style: theme.textTheme.body2,
child: child, child: new MediaQuery.removePadding(
context: context,
removeTop: true,
child: child,
),
), ),
), ),
); );

View file

@ -37,13 +37,15 @@ enum _ScaffoldSlot {
class _ScaffoldLayout extends MultiChildLayoutDelegate { class _ScaffoldLayout extends MultiChildLayoutDelegate {
_ScaffoldLayout({ _ScaffoldLayout({
@required this.padding,
@required this.statusBarHeight, @required this.statusBarHeight,
@required this.bottomPadding,
@required this.endPadding, // for floating action button
@required this.textDirection, @required this.textDirection,
}); });
final EdgeInsets padding;
final double statusBarHeight; final double statusBarHeight;
final double bottomPadding;
final double endPadding;
final TextDirection textDirection; final TextDirection textDirection;
@override @override
@ -56,7 +58,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
// so the app bar's shadow is drawn on top of the body. // so the app bar's shadow is drawn on top of the body.
final BoxConstraints fullWidthConstraints = looseConstraints.tighten(width: size.width); 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 contentTop = 0.0;
double contentBottom = bottom; double contentBottom = bottom;
@ -72,7 +74,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
} }
if (hasChild(_ScaffoldSlot.persistentFooter)) { 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; contentBottom -= persistentFooterHeight;
positionChild(_ScaffoldSlot.persistentFooter, new Offset(0.0, contentBottom)); positionChild(_ScaffoldSlot.persistentFooter, new Offset(0.0, contentBottom));
} }
@ -102,7 +108,11 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
Size snackBarSize = Size.zero; Size snackBarSize = Size.zero;
if (hasChild(_ScaffoldSlot.bottomSheet)) { 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)); 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); assert(textDirection != null);
switch (textDirection) { switch (textDirection) {
case TextDirection.rtl: case TextDirection.rtl:
fabX = _kFloatingActionButtonMargin; fabX = _kFloatingActionButtonMargin + endPadding;
break; break;
case TextDirection.ltr: case TextDirection.ltr:
fabX = size.width - fabSize.width - _kFloatingActionButtonMargin; fabX = size.width - fabSize.width - _kFloatingActionButtonMargin - endPadding;
break; break;
} }
double fabY = contentBottom - fabSize.height - _kFloatingActionButtonMargin; double fabY = contentBottom - fabSize.height - _kFloatingActionButtonMargin;
@ -144,8 +154,9 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
@override @override
bool shouldRelayout(_ScaffoldLayout oldDelegate) { bool shouldRelayout(_ScaffoldLayout oldDelegate) {
return oldDelegate.padding != padding return oldDelegate.statusBarHeight != statusBarHeight
|| oldDelegate.statusBarHeight != statusBarHeight || oldDelegate.bottomPadding != bottomPadding
|| oldDelegate.endPadding != endPadding
|| oldDelegate.textDirection != textDirection; || oldDelegate.textDirection != textDirection;
} }
} }
@ -336,10 +347,13 @@ class Scaffold extends StatefulWidget {
/// A set of buttons that are displayed at the bottom of the scaffold. /// A set of buttons that are displayed at the bottom of the scaffold.
/// ///
/// Typically this is a list of [FlatButton] widgets. These buttons are /// 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]. /// These widgets will be wrapped in a [ButtonBar].
/// ///
/// The [persistentFooterButtons] are rendered above the
/// [bottomNavigationBar] but below the [body].
///
/// See also: /// See also:
/// ///
/// * <https://material.google.com/components/buttons.html#buttons-persistent-footer-buttons> /// * <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 /// Snack bars slide from underneath the bottom navigation bar while bottom
/// sheets are stacked on top. /// sheets are stacked on top.
///
/// The [bottomNavigationBar] is rendered below the [persistentFooterButtons]
/// and the [body].
final Widget bottomNavigationBar; final Widget bottomNavigationBar;
/// Whether the [body] (and other floating widgets) should size themselves to /// Whether the [body] (and other floating widgets) should size themselves to
@ -747,18 +764,36 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
super.dispose(); super.dispose();
} }
void _addIfNonNull(List<LayoutId> children, Widget child, Object childId) { void _addIfNonNull(List<LayoutId> children, Widget child, Object childId, {
if (child != null) @required bool removeLeftPadding,
children.add(new LayoutId(child: child, id: childId)); @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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(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); final ThemeData themeData = Theme.of(context);
if (!widget.resizeToAvoidBottomPadding) final TextDirection textDirection = Directionality.of(context);
padding = new EdgeInsets.fromLTRB(padding.left, padding.top, padding.right, 0.0);
if (_snackBars.isNotEmpty) { if (_snackBars.isNotEmpty) {
final ModalRoute<dynamic> route = ModalRoute.of(context); final ModalRoute<dynamic> route = ModalRoute.of(context);
@ -777,7 +812,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
final List<LayoutId> children = <LayoutId>[]; 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) { if (widget.appBar != null) {
final double topPadding = widget.primary ? padding.top : 0.0; final double topPadding = widget.primary ? padding.top : 0.0;
@ -793,16 +835,28 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
), ),
), ),
_ScaffoldSlot.appBar, _ScaffoldSlot.appBar,
removeLeftPadding: false,
removeTopPadding: false,
removeRightPadding: false,
removeBottomPadding: true,
); );
} }
if (_snackBars.isNotEmpty) if (_snackBars.isNotEmpty) {
_addIfNonNull(children, _snackBars.first._widget, _ScaffoldSlot.snackBar); _addIfNonNull(
children,
_snackBars.first._widget,
_ScaffoldSlot.snackBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
);
}
if (widget.persistentFooterButtons != null) { if (widget.persistentFooterButtons != null) {
children.add(new LayoutId( _addIfNonNull(
id: _ScaffoldSlot.persistentFooter, children,
child: new Container( new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
border: new Border( border: new Border(
top: new BorderSide( top: new BorderSide(
@ -810,20 +864,30 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
), ),
), ),
), ),
child: new ButtonTheme.bar( child: new SafeArea(
child: new ButtonBar( child: new ButtonTheme.bar(
children: widget.persistentFooterButtons child: new ButtonBar(
children: widget.persistentFooterButtons
),
), ),
), ),
), ),
)); _ScaffoldSlot.persistentFooter,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
);
} }
if (widget.bottomNavigationBar != null) { if (widget.bottomNavigationBar != null) {
children.add(new LayoutId( _addIfNonNull(
id: _ScaffoldSlot.bottomNavigationBar, children,
child: widget.bottomNavigationBar, widget.bottomNavigationBar,
)); _ScaffoldSlot.bottomNavigationBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
);
} }
if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) { if (_currentBottomSheet != null || _dismissedBottomSheets.isNotEmpty) {
@ -836,39 +900,73 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
children: bottomSheets, children: bottomSheets,
alignment: FractionalOffset.bottomCenter, alignment: FractionalOffset.bottomCenter,
); );
_addIfNonNull(children, stack, _ScaffoldSlot.bottomSheet); _addIfNonNull(
children,
stack,
_ScaffoldSlot.bottomSheet,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
);
} }
children.add(new LayoutId( _addIfNonNull(
id: _ScaffoldSlot.floatingActionButton, children,
child: new _FloatingActionButtonTransition( new _FloatingActionButtonTransition(
child: widget.floatingActionButton, child: widget.floatingActionButton,
) ),
)); _ScaffoldSlot.floatingActionButton,
removeLeftPadding: true,
removeTopPadding: true,
removeRightPadding: true,
removeBottomPadding: true,
);
if (themeData.platform == TargetPlatform.iOS) { if (themeData.platform == TargetPlatform.iOS) {
children.add(new LayoutId( _addIfNonNull(
id: _ScaffoldSlot.statusBar, children,
child: new GestureDetector( new GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: _handleStatusBarTap, onTap: _handleStatusBarTap,
// iOS accessibility automatically adds scroll-to-top to the clock in the status bar // iOS accessibility automatically adds scroll-to-top to the clock in the status bar
excludeFromSemantics: true, excludeFromSemantics: true,
) ),
)); _ScaffoldSlot.statusBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: true,
);
} }
if (widget.drawer != null) { if (widget.drawer != null) {
assert(hasDrawer); assert(hasDrawer);
children.add(new LayoutId( _addIfNonNull(
id: _ScaffoldSlot.drawer, children,
child: new DrawerController( new DrawerController(
key: _drawerKey, key: _drawerKey,
child: widget.drawer, 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( return new _ScaffoldScope(
hasDrawer: hasDrawer, hasDrawer: hasDrawer,
child: new PrimaryScrollController( child: new PrimaryScrollController(
@ -878,9 +976,10 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
child: new CustomMultiChildLayout( child: new CustomMultiChildLayout(
children: children, children: children,
delegate: new _ScaffoldLayout( delegate: new _ScaffoldLayout(
padding: padding,
statusBarHeight: padding.top, statusBarHeight: padding.top,
textDirection: Directionality.of(context), bottomPadding: widget.resizeToAvoidBottomPadding ? padding.bottom : 0.0,
endPadding: endPadding,
textDirection: textDirection,
), ),
), ),
), ),

View file

@ -336,6 +336,11 @@ abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> exten
/// For more complex animations, you'll likely want to use a subclass of /// For more complex animations, you'll likely want to use a subclass of
/// [AnimatedWidget] such as the [DecoratedBoxTransition] or use your own /// [AnimatedWidget] such as the [DecoratedBoxTransition] or use your own
/// [AnimationController]. /// [AnimationController].
///
/// See also:
///
/// * [AnimatedPadding], which is a subset of this widget that only
/// supports animating the [padding].
class AnimatedContainer extends ImplicitlyAnimatedWidget { class AnimatedContainer extends ImplicitlyAnimatedWidget {
/// Creates a container that animates its parameters implicitly. /// 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 /// Animated version of [Positioned] which automatically transitions the child's
/// position over a given duration whenever the given position changes. /// position over a given duration whenever the given position changes.
/// ///

View file

@ -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 @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other.runtimeType != runtimeType) if (other.runtimeType != runtimeType)
@ -148,6 +182,44 @@ class MediaQuery extends InheritedWidget {
assert(data != null), assert(data != null),
super(key: key, child: child); 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. /// Contains information about the current media.
/// ///
/// For example, the [MediaQueryData.size] property contains the width and /// For example, the [MediaQueryData.size] property contains the width and

View 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'));
}
}

View file

@ -64,6 +64,7 @@ export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart'; export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/safe_area.dart';
export 'src/widgets/scroll_activity.dart'; export 'src/widgets/scroll_activity.dart';
export 'src/widgets/scroll_configuration.dart'; export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_context.dart'; export 'src/widgets/scroll_context.dart';

View file

@ -278,7 +278,7 @@ void main() {
expect(tester.getTopRight(find.text('X')).dx, 800.0 - 72.0); 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 // The app bar's title should be constrained to fit within the available space
// between the leading and actions widgets. // 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)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.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));
});
} }

View file

@ -527,4 +527,100 @@ void main() {
semantics.dispose(); 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));
});
} }

View 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));
});
}

View 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));
});
}