Commit 4c83ea8b authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

SafeArea (#12292)

* SafeArea

* AnimatedSafeArea

* AppBar test

* Apply feedback
parent 9646e1fb
......@@ -337,11 +337,15 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
left: _kNavBarEdgePadding,
right: _kNavBarEdgePadding,
),
child: new NavigationToolbar(
leading: styledLeading,
middle: animatedStyledMiddle,
trailing: styledTrailing,
centerMiddle: true,
child: new MediaQuery.removePadding(
context: context,
removeTop: true,
child: new NavigationToolbar(
leading: styledLeading,
middle: animatedStyledMiddle,
trailing: styledTrailing,
centerMiddle: true,
),
),
),
),
......
......@@ -54,10 +54,9 @@ class CupertinoPageScaffold extends StatelessWidget {
if (topPadding > 0.0) {
final EdgeInsets mediaQueryPadding = MediaQuery.of(context).padding;
topPadding += mediaQueryPadding.top;
childWithMediaQuery = new MediaQuery(
data: MediaQuery.of(context).copyWith(
padding: mediaQueryPadding.copyWith(top: 0.0),
),
childWithMediaQuery = new MediaQuery.removePadding(
context: context,
removeTop: true,
child: child,
);
}
......
......@@ -441,8 +441,8 @@ class _AppBarState extends State<AppBar> {
// The padding applies to the toolbar and tabbar, not the flexible space.
if (widget.primary) {
appBar = new Padding(
padding: new EdgeInsets.only(top: MediaQuery.of(context).padding.top),
appBar = new SafeArea(
top: true,
child: appBar,
);
}
......
......@@ -93,7 +93,11 @@ class DrawerHeader extends StatelessWidget {
curve: curve,
child: child == null ? null : new DefaultTextStyle(
style: theme.textTheme.body2,
child: child,
child: new MediaQuery.removePadding(
context: context,
removeTop: true,
child: child,
),
),
),
);
......
......@@ -336,6 +336,11 @@ abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> exten
/// For more complex animations, you'll likely want to use a subclass of
/// [AnimatedWidget] such as the [DecoratedBoxTransition] or use your own
/// [AnimationController].
///
/// See also:
///
/// * [AnimatedPadding], which is a subset of this widget that only
/// supports animating the [padding].
class AnimatedContainer extends ImplicitlyAnimatedWidget {
/// Creates a container that animates its parameters implicitly.
///
......@@ -479,6 +484,66 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer>
}
}
/// Animated version of [Padding] which automatically transitions the
/// indentation over a given duration whenever the given inset changes.
///
/// See also:
///
/// * [AnimatedContainer], which can transition more values at once.
class AnimatedPadding extends ImplicitlyAnimatedWidget {
/// Creates a widget that insets its child by a value that animates
/// implicitly.
///
/// The [padding], [curve], and [duration] arguments must not be null.
AnimatedPadding({
Key key,
@required this.padding,
this.child,
Curve curve: Curves.linear,
@required Duration duration,
}) : assert(padding != null),
assert(padding.isNonNegative),
super(key: key, curve: curve, duration: duration);
/// The amount of space by which to inset the child.
final EdgeInsetsGeometry padding;
/// The widget below this widget in the tree.
final Widget child;
@override
_AnimatedPaddingState createState() => new _AnimatedPaddingState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
}
}
class _AnimatedPaddingState extends AnimatedWidgetBaseState<AnimatedPadding> {
EdgeInsetsGeometryTween _padding;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_padding = visitor(_padding, widget.padding, (dynamic value) => new EdgeInsetsGeometryTween(begin: value));
}
@override
Widget build(BuildContext context) {
return new Padding(
padding: _padding.evaluate(animation),
child: widget.child,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<EdgeInsetsGeometryTween>('padding', _padding, defaultValue: null));
}
}
/// Animated version of [Positioned] which automatically transitions the child's
/// position over a given duration whenever the given position changes.
///
......
......@@ -98,6 +98,40 @@ class MediaQueryData {
);
}
/// Creates a copy of this media query data but with the given paddings
/// replaced with zero.
///
/// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
/// must not be null. If all four are false (the default) then this
/// [MediaQueryData] is returned unmodified.
///
/// See also:
///
/// * [new MediaQuery.removePadding], which uses this method to remove padding
/// from the ambient [MediaQuery].
/// * [SafeArea], which both removes the padding from the [MediaQuery] and
/// adds a [Padding] widget.
MediaQueryData removePadding({
bool removeLeft: false,
bool removeTop: false,
bool removeRight: false,
bool removeBottom: false,
}) {
if (!(removeLeft || removeTop || removeRight || removeBottom))
return this;
return new MediaQueryData(
size: size,
devicePixelRatio: devicePixelRatio,
textScaleFactor: textScaleFactor,
padding: padding.copyWith(
left: removeLeft ? 0.0 : null,
top: removeTop ? 0.0 : null,
right: removeRight ? 0.0 : null,
bottom: removeBottom ? 0.0 : null,
),
);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
......@@ -148,6 +182,44 @@ class MediaQuery extends InheritedWidget {
assert(data != null),
super(key: key, child: child);
/// Creates a new [MediaQuery] that inherits from the ambient [MediaQuery] from
/// the given context, but removes the specified paddings.
///
/// The [context] argument is required, must not be null, and must have a
/// [MediaQuery] in scope.
///
/// The `removeLeft`, `removeTop`, `removeRight`, and `removeBottom` arguments
/// must not be null. If all four are false (the default) then the returned
/// [MediaQuery] reuses the ambient [MediaQueryData] unmodified, which is not
/// particularly useful.
///
/// The [child] argument is required and must not be null.
///
/// See also:
///
/// * [SafeArea], which both removes the padding from the [MediaQuery] and
/// adds a [Padding] widget.
factory MediaQuery.removePadding({
Key key,
@required BuildContext context,
bool removeLeft: false,
bool removeTop: false,
bool removeRight: false,
bool removeBottom: false,
@required Widget child,
}) {
return new MediaQuery(
key: key,
data: MediaQuery.of(context).removePadding(
removeLeft: removeLeft,
removeTop: removeTop,
removeRight: removeRight,
removeBottom: removeBottom,
),
child: child,
);
}
/// Contains information about the current media.
///
/// For example, the [MediaQueryData.size] property contains the width and
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
import 'media_query.dart';
/// A widget that insets its child by sufficient padding to avoid
/// intrusions by the operating system.
///
/// For example, this will indent the child by enough to avoid the status bar at
/// the top of the screen.
///
/// It will also indent the child by the amount necessary to avoid The Notch on
/// the iPhone X, or other similar creative physical features of the display.
///
/// See also:
///
/// * [Padding], for insetting widgets in general.
/// * [MediaQuery], from which the window padding is obtained.
/// * [dart:ui.Window.padding], which reports the padding from the operating
/// system.
class SafeArea extends StatelessWidget {
/// Creates a widget that avoids operating system interfaces.
///
/// The [left], [top], [right], and [bottom] arguments must not be null.
const SafeArea({
Key key,
this.left: true,
this.top: true,
this.right: true,
this.bottom: true,
@required this.child,
}) : assert(left != null),
assert(top != null),
assert(right != null),
assert(bottom != null),
super(key: key);
/// Whether to avoid system intrusions on the left.
final bool left;
/// Whether to avoid system intrusions at the top of the screen, typically the
/// system status bar.
final bool top;
/// Whether to avoid system intrusions on the right.
final bool right;
/// Whether to avoid system intrusions on the bottom side of the screen.
final bool bottom;
/// The widget below this widget in the tree.
///
/// The padding on the [MediaQuery] for the [child] will be suitably adjusted
/// to zero out any sides that were avoided by this widget.
final Widget child;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
final EdgeInsets padding = MediaQuery.of(context).padding;
return new Padding(
padding: new EdgeInsets.only(
left: left ? padding.left : 0.0,
top: top ? padding.top : 0.0,
right: right ? padding.right : 0.0,
bottom: bottom ? padding.bottom : 0.0,
),
child: new MediaQuery.removePadding(
context: context,
removeLeft: left,
removeTop: top,
removeRight: right,
removeBottom: bottom,
child: child,
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new FlagProperty('left', value: left, ifTrue: 'avoid left padding'));
description.add(new FlagProperty('top', value: left, ifTrue: 'avoid top padding'));
description.add(new FlagProperty('right', value: left, ifTrue: 'avoid right padding'));
description.add(new FlagProperty('bottom', value: left, ifTrue: 'avoid bottom padding'));
}
}
......@@ -64,6 +64,7 @@ export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/safe_area.dart';
export 'src/widgets/scroll_activity.dart';
export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_context.dart';
......
......@@ -278,7 +278,7 @@ void main() {
expect(tester.getTopRight(find.text('X')).dx, 800.0 - 72.0);
});
testWidgets('AppBar centerTitle:false title overflow OK ', (WidgetTester tester) async {
testWidgets('AppBar centerTitle:false title overflow OK', (WidgetTester tester) async {
// The app bar's title should be constrained to fit within the available space
// between the leading and actions widgets.
......@@ -1085,4 +1085,64 @@ void main() {
expect(tester.renderObject<RenderBox>(find.byKey(key)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0));
});
testWidgets('AppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async {
const MediaQueryData topPadding100 = const MediaQueryData(padding: const EdgeInsets.only(top: 100.0));
final Key leadingKey = new UniqueKey();
final Key titleKey = new UniqueKey();
final Key trailingKey = new UniqueKey();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.rtl,
child: new MediaQuery(
data: topPadding100,
child: new Scaffold(
primary: false,
appBar: new AppBar(
leading: new Placeholder(key: leadingKey),
title: new Placeholder(key: titleKey),
actions: <Widget>[ new Placeholder(key: trailingKey) ],
),
),
),
),
);
expect(tester.getTopLeft(find.byType(AppBar)), const Offset(0.0, 0.0));
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(800.0 - 56.0, 100.0));
expect(tester.getTopLeft(find.byKey(titleKey)), const Offset(420.0, 100.0));
expect(tester.getTopLeft(find.byKey(trailingKey)), const Offset(4.0, 100.0));
});
testWidgets('SliverAppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async {
const MediaQueryData topPadding100 = const MediaQueryData(padding: const EdgeInsets.only(top: 100.0));
final Key leadingKey = new UniqueKey();
final Key titleKey = new UniqueKey();
final Key trailingKey = new UniqueKey();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.rtl,
child: new MediaQuery(
data: topPadding100,
child: new CustomScrollView(
primary: true,
slivers: <Widget>[
new SliverAppBar(
leading: new Placeholder(key: leadingKey),
title: new Placeholder(key: titleKey),
actions: <Widget>[ new Placeholder(key: trailingKey) ],
),
],
),
),
),
);
expect(tester.getTopLeft(find.byType(AppBar)), const Offset(0.0, 0.0));
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(800.0 - 56.0, 100.0));
expect(tester.getTopLeft(find.byKey(titleKey)), const Offset(420.0, 100.0));
expect(tester.getTopLeft(find.byKey(trailingKey)), const Offset(4.0, 100.0));
});
}
......@@ -527,4 +527,100 @@ void main() {
semantics.dispose();
});
testWidgets('Scaffold and extreme window padding', (WidgetTester tester) async {
final Key appBar = new UniqueKey();
final Key body = new UniqueKey();
final Key floatingActionButton = new UniqueKey();
final Key persistentFooterButton = new UniqueKey();
final Key drawer = new UniqueKey();
final Key bottomNavigationBar = new UniqueKey();
final Key insideAppBar = new UniqueKey();
final Key insideBody = new UniqueKey();
final Key insideFloatingActionButton = new UniqueKey();
final Key insidePersistentFooterButton = new UniqueKey();
final Key insideDrawer = new UniqueKey();
final Key insideBottomNavigationBar = new UniqueKey();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.rtl,
child: new MediaQuery(
data: const MediaQueryData(
padding: const EdgeInsets.only(
left: 20.0,
top: 30.0,
right: 50.0,
bottom: 70.0,
),
),
child: new Scaffold(
appBar: new PreferredSize(
preferredSize: const Size(11.0, 13.0),
child: new Container(
key: appBar,
child: new SafeArea(
child: new Placeholder(key: insideAppBar),
),
),
),
body: new Container(
key: body,
child: new SafeArea(
child: new Placeholder(key: insideBody),
),
),
floatingActionButton: new SizedBox(
key: floatingActionButton,
width: 77.0,
height: 77.0,
child: new SafeArea(
child: new Placeholder(key: insideFloatingActionButton),
),
),
persistentFooterButtons: <Widget>[
new SizedBox(
key: persistentFooterButton,
width: 100.0,
height: 90.0,
child: new SafeArea(
child: new Placeholder(key: insidePersistentFooterButton),
),
),
],
drawer: new Container(
key: drawer,
width: 204.0,
child: new SafeArea(
child: new Placeholder(key: insideDrawer),
),
),
bottomNavigationBar: new SizedBox(
key: bottomNavigationBar,
height: 55.0,
child: new SafeArea(
child: new Placeholder(key: insideBottomNavigationBar),
),
),
),
),
),
);
// open drawer
await tester.flingFrom(const Offset(795.0, 5.0), const Offset(-200.0, 0.0), 10.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.getRect(find.byKey(appBar)), new Rect.fromLTRB(0.0, 0.0, 800.0, 43.0));
expect(tester.getRect(find.byKey(body)), new Rect.fromLTRB(0.0, 43.0, 800.0, 368.0));
expect(tester.getRect(find.byKey(floatingActionButton)), new Rect.fromLTRB(36.0, 275.0, 113.0, 352.0));
expect(tester.getRect(find.byKey(persistentFooterButton)), new Rect.fromLTRB(28.0, 377.0, 128.0, 467.0));
expect(tester.getRect(find.byKey(drawer)), new Rect.fromLTRB(596.0, 0.0, 800.0, 600.0));
expect(tester.getRect(find.byKey(bottomNavigationBar)), new Rect.fromLTRB(0.0, 475.0, 800.0, 530.0));
expect(tester.getRect(find.byKey(insideAppBar)), new Rect.fromLTRB(20.0, 30.0, 750.0, 43.0));
expect(tester.getRect(find.byKey(insideBody)), new Rect.fromLTRB(20.0, 43.0, 750.0, 368.0));
expect(tester.getRect(find.byKey(insideFloatingActionButton)), new Rect.fromLTRB(36.0, 275.0, 113.0, 352.0));
expect(tester.getRect(find.byKey(insidePersistentFooterButton)), new Rect.fromLTRB(28.0, 377.0, 128.0, 467.0));
expect(tester.getRect(find.byKey(insideDrawer)), new Rect.fromLTRB(596.0, 30.0, 750.0, 530.0));
expect(tester.getRect(find.byKey(insideBottomNavigationBar)), new Rect.fromLTRB(20.0, 475.0, 750.0, 530.0));
});
}
// 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));
});
}
// 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));
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment