diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index b774b186c21..60779552613 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -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, + ), ), ), ), diff --git a/packages/flutter/lib/src/cupertino/page_scaffold.dart b/packages/flutter/lib/src/cupertino/page_scaffold.dart index 4a71147e6b8..6a8c32bef4c 100644 --- a/packages/flutter/lib/src/cupertino/page_scaffold.dart +++ b/packages/flutter/lib/src/cupertino/page_scaffold.dart @@ -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, ); } diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 231152256f5..c683f50ee66 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -441,8 +441,8 @@ class _AppBarState extends State { // 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, ); } diff --git a/packages/flutter/lib/src/material/drawer_header.dart b/packages/flutter/lib/src/material/drawer_header.dart index b56b8c0852c..e5280467776 100644 --- a/packages/flutter/lib/src/material/drawer_header.dart +++ b/packages/flutter/lib/src/material/drawer_header.dart @@ -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, + ), ), ), ); diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 6c3731c2741..f4a8c5ebd74 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -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: /// /// * @@ -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 with TickerProviderStateMixin { super.dispose(); } - void _addIfNonNull(List children, Widget child, Object childId) { - if (child != null) - children.add(new LayoutId(child: child, id: childId)); + void _addIfNonNull(List 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 route = ModalRoute.of(context); @@ -777,7 +812,14 @@ class ScaffoldState extends State with TickerProviderStateMixin { final List children = []; - _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 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 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 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 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, ), ), ), diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index d6084bd92d1..6434f3adb98 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -336,6 +336,11 @@ abstract class AnimatedWidgetBaseState 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 } } +/// 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('padding', padding)); + } +} + +class _AnimatedPaddingState extends AnimatedWidgetBaseState { + EdgeInsetsGeometryTween _padding; + + @override + void forEachTween(TweenVisitor 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('padding', _padding, defaultValue: null)); + } +} + /// Animated version of [Positioned] which automatically transitions the child's /// position over a given duration whenever the given position changes. /// diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index 356fbee190e..12cc7d1b8f7 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -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 diff --git a/packages/flutter/lib/src/widgets/safe_area.dart b/packages/flutter/lib/src/widgets/safe_area.dart new file mode 100644 index 00000000000..39e2ef313ae --- /dev/null +++ b/packages/flutter/lib/src/widgets/safe_area.dart @@ -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')); + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 085f0caf197..eb16c3748e6 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -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'; diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 4fd0bc72766..cd38e344a5a 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.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(find.byKey(key)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); expect(tester.renderObject(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: [ 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: [ + new SliverAppBar( + leading: new Placeholder(key: leadingKey), + title: new Placeholder(key: titleKey), + actions: [ 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)); + }); } diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index 27446ddaa09..fe21c6bf7d9 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -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: [ + 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)); + }); } diff --git a/packages/flutter/test/widgets/animated_padding_test.dart b/packages/flutter/test/widgets/animated_padding_test.dart new file mode 100644 index 00000000000..e28ad56a2f3 --- /dev/null +++ b/packages/flutter/test/widgets/animated_padding_test.dart @@ -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)); + }); +} diff --git a/packages/flutter/test/widgets/safe_area_test.dart b/packages/flutter/test/widgets/safe_area_test.dart new file mode 100644 index 00000000000..3eb7d3ba7ac --- /dev/null +++ b/packages/flutter/test/widgets/safe_area_test.dart @@ -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)); + }); +}