SafeArea (#12292)

* SafeArea

* AnimatedSafeArea

* AppBar test

* Apply feedback
diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart
index b774b18..6077955 100644
--- a/packages/flutter/lib/src/cupertino/nav_bar.dart
+++ b/packages/flutter/lib/src/cupertino/nav_bar.dart
@@ -337,11 +337,15 @@
             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 4a71147..6a8c32b 100644
--- a/packages/flutter/lib/src/cupertino/page_scaffold.dart
+++ b/packages/flutter/lib/src/cupertino/page_scaffold.dart
@@ -54,10 +54,9 @@
       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 2311522..c683f50 100644
--- a/packages/flutter/lib/src/material/app_bar.dart
+++ b/packages/flutter/lib/src/material/app_bar.dart
@@ -441,8 +441,8 @@
 
     // 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 b56b8c0..e528046 100644
--- a/packages/flutter/lib/src/material/drawer_header.dart
+++ b/packages/flutter/lib/src/material/drawer_header.dart
@@ -93,7 +93,11 @@
         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 6c3731c..f4a8c5e 100644
--- a/packages/flutter/lib/src/material/scaffold.dart
+++ b/packages/flutter/lib/src/material/scaffold.dart
@@ -37,13 +37,15 @@
 
 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 @@
     // 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 @@
     }
 
     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 @@
     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 @@
       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 @@
 
   @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 @@
   /// 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 @@
   ///
   /// 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 @@
     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 @@
 
     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 @@
           ),
         ),
         _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 @@
               ),
             ),
           ),
-          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 @@
         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 @@
           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 d6084bd..6434f3a 100644
--- a/packages/flutter/lib/src/widgets/implicit_animations.dart
+++ b/packages/flutter/lib/src/widgets/implicit_animations.dart
@@ -336,6 +336,11 @@
 /// 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 @@
   }
 }
 
+/// 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.
 ///
diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart
index 356fbee..12cc7d1 100644
--- a/packages/flutter/lib/src/widgets/media_query.dart
+++ b/packages/flutter/lib/src/widgets/media_query.dart
@@ -98,6 +98,40 @@
     );
   }
 
+  /// 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 @@
        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 0000000..39e2ef3
--- /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 085f0ca..eb16c37 100644
--- a/packages/flutter/lib/widgets.dart
+++ b/packages/flutter/lib/widgets.dart
@@ -64,6 +64,7 @@
 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 4fd0bc7..cd38e34 100644
--- a/packages/flutter/test/material/app_bar_test.dart
+++ b/packages/flutter/test/material/app_bar_test.dart
@@ -278,7 +278,7 @@
     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 @@
     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));
+  });
 }
diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart
index 27446dd..fe21c6b 100644
--- a/packages/flutter/test/material/scaffold_test.dart
+++ b/packages/flutter/test/material/scaffold_test.dart
@@ -527,4 +527,100 @@
 
     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));
+  });
 }
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 0000000..e28ad56
--- /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 0000000..3eb7d3b
--- /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));
+  });
+}