* 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,
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,
),
),
),
),

View file

@ -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,
);
}

View file

@ -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,
);
}

View file

@ -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,
),
),
),
);

View file

@ -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,
),
),
),

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
/// [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.
///

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
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

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/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';

View file

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

View file

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

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