Appbar slivers. (#7631)

diff --git a/packages/flutter/lib/rendering.dart b/packages/flutter/lib/rendering.dart
index 1d851c6..d579bdf 100644
--- a/packages/flutter/lib/rendering.dart
+++ b/packages/flutter/lib/rendering.dart
@@ -45,6 +45,7 @@
 export 'src/rendering/semantics.dart';
 export 'src/rendering/shifted_box.dart';
 export 'src/rendering/sliver.dart';
+export 'src/rendering/sliver_app_bar.dart';
 export 'src/rendering/sliver_block.dart';
 export 'src/rendering/stack.dart';
 export 'src/rendering/table.dart';
diff --git a/packages/flutter/lib/src/rendering/sliver_app_bar.dart b/packages/flutter/lib/src/rendering/sliver_app_bar.dart
new file mode 100644
index 0000000..d8efac1
--- /dev/null
+++ b/packages/flutter/lib/src/rendering/sliver_app_bar.dart
@@ -0,0 +1,308 @@
+// Copyright 2016 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 'dart:math' as math;
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:meta/meta.dart';
+import 'package:vector_math/vector_math_64.dart';
+
+import 'box.dart';
+import 'binding.dart';
+import 'object.dart';
+import 'sliver.dart';
+
+abstract class RenderSliverAppBar extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
+  RenderSliverAppBar({ RenderBox child }) {
+    this.child = child;
+  }
+
+  double get maxExtent;
+
+  /// The intrinsic size of the child as of the last time the sliver was laid out.
+  ///
+  /// If the render object is dirty (i.e. if [markNeedsLayout] has been called,
+  /// or if the object was newly created), then the returned value will be stale
+  /// until [layoutChild] has been called.
+  @protected
+  double get minExtent => _minExtent;
+  double _minExtent;
+
+  @protected
+  double get childExtent {
+    if (child == null)
+      return 0.0;
+    assert(child.hasSize);
+    assert(constraints.axis != null);
+    switch (constraints.axis) {
+      case Axis.vertical:
+        return child.size.height;
+      case Axis.horizontal:
+        return child.size.width;
+    }
+    return null;
+  }
+
+  @protected
+  double _getChildIntrinsicExtent() {
+    if (child == null)
+      return 0.0;
+    assert(child != null);
+    assert(constraints.axis != null);
+    switch (constraints.axis) {
+      case Axis.vertical:
+        return child.getMinIntrinsicHeight(constraints.crossAxisExtent);
+      case Axis.horizontal:
+        return child.getMinIntrinsicWidth(constraints.crossAxisExtent);
+    }
+    return null;
+  }
+
+  /// The last value that we passed to updateChild().
+  double _lastShrinkOffset;
+
+  /// Called during layout if the shrink offset has changed.
+  ///
+  /// During this callback, the [child] can be set, mutated, or replaced.
+  @protected
+  void updateChild(double shrinkOffset) { }
+
+  /// Flag the current child as stale and needing updating even if the shrink
+  /// offset has not changed.
+  ///
+  /// Call this whenever [updateChild] would change or mutate the child even if
+  /// given the same `shrinkOffset` as the last time it was called.
+  ///
+  /// This must be implemented by [RenderSliverAppBar] subclasses such that the
+  /// next layout after this call will result in [updateChild] being called.
+  @protected
+  void markNeedsUpdate() {
+    markNeedsLayout();
+    _lastShrinkOffset = null;
+  }
+
+  void layoutChild(double scrollOffset, double maxExtent) {
+    assert(maxExtent != null);
+    final double shrinkOffset = math.min(scrollOffset, maxExtent);
+    if (shrinkOffset != _lastShrinkOffset) {
+      invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
+        assert(constraints == this.constraints);
+        updateChild(shrinkOffset);
+        _minExtent = _getChildIntrinsicExtent();
+      });
+      _lastShrinkOffset = shrinkOffset;
+    }
+    assert(_minExtent != null);
+    assert(() {
+      if (_minExtent <= maxExtent)
+        return true;
+      throw new FlutterError(
+        'The maxExtent for this $runtimeType is less than the child\'s intrinsic extent.\n'
+        'The specified maxExtent was: ${maxExtent.toStringAsFixed(1)}\n'
+        'The child was updated with shrink offset: ${shrinkOffset.toStringAsFixed(1)}\n'
+        'The actual measured intrinsic extent of the child was: ${_minExtent.toStringAsFixed(1)}\n'
+      );
+    });
+    child?.layout(
+      constraints.asBoxConstraints(maxExtent: math.max(_minExtent, maxExtent - shrinkOffset)),
+      parentUsesSize: true,
+    );
+  }
+
+  /// Returns the distance from the leading _visible_ edge of the sliver to the
+  /// side of the child closest to that edge, in the scroll axis direction.
+  ///
+  /// For example, if the [constraints] describe this sliver as having an axis
+  /// direction of [AxisDirection.down], then this is the distance from the top
+  /// of the visible portion of the sliver to the top of the child. If the child
+  /// is scrolled partially off the top of the viewport, then this will be
+  /// negative. On the other hand, if the [constraints] describe this sliver as
+  /// having an axis direction of [AxisDirection.up], then this is the distance
+  /// from the bottom of the visible portion of the sliver to the bottom of the
+  /// child. In both cases, this is the direction of increasing
+  /// [SliverConstraints.scrollOffset].
+  ///
+  /// Calling this when the child is not visible is not valid.
+  ///
+  /// The argument must be the value of the [child] property.
+  ///
+  /// This must be implemented by [RenderSliverAppBar] subclasses.
+  ///
+  /// If there is no child, this should return 0.0.
+  @override
+  double childPosition(@checked RenderObject child) => super.childPosition(child);
+
+  @override
+  bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
+    assert(geometry.hitTestExtent > 0.0);
+    if (child != null)
+      return hitTestBoxChild(result, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition);
+    return false;
+  }
+
+  @override
+  void applyPaintTransform(RenderObject child, Matrix4 transform) {
+    assert(child != null);
+    assert(child == this.child);
+    applyPaintTransformForBoxChild(child, transform);
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    if (child != null && geometry.visible) {
+      assert(constraints.axisDirection != null);
+      switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
+        case AxisDirection.up:
+          offset += new Offset(0.0, geometry.paintExtent - childPosition(child) - childExtent);
+          break;
+        case AxisDirection.down:
+          offset += new Offset(0.0, childPosition(child));
+          break;
+        case AxisDirection.left:
+          offset += new Offset(geometry.paintExtent - childPosition(child) - childExtent, 0.0);
+          break;
+        case AxisDirection.right:
+          offset += new Offset(childPosition(child), 0.0);
+          break;
+      }
+      context.paintChild(child, offset);
+    }
+  }
+
+  @override
+  void debugFillDescription(List<String> description) {
+    super.debugFillDescription(description);
+    try {
+      description.add('maxExtent: ${maxExtent.toStringAsFixed(1)}');
+    } catch (e) {
+      description.add('maxExtent: EXCEPTION (${e.runtimeType}) WHILE COMPUTING MAX EXTENT');
+    }
+    try {
+      description.add('child position: ${childPosition(child).toStringAsFixed(1)}');
+    } catch (e) {
+      description.add('child position: EXCEPTION (${e.runtimeType}) WHILE COMPUTING CHILD POSITION');
+    }
+  }
+}
+
+/// A sliver with a [RenderBox] child which scrolls normally, except that when
+/// it hits the leading edge (typically the top) of the viewport, it shrinks to
+/// a minimum size before continuing to scroll.
+///
+/// This sliver makes no effort to avoid overlapping other content.
+abstract class RenderSliverScrollingAppBar extends RenderSliverAppBar {
+  RenderSliverScrollingAppBar({
+    RenderBox child,
+  }) : super(child: child);
+
+  // Distance from our leading edge to the child's leading edge, in the axis
+  // direction. Negative if we're scrolled off the top.
+  double _childPosition;
+
+  @override
+  void performLayout() {
+    final double maxExtent = this.maxExtent;
+    layoutChild(constraints.scrollOffset, maxExtent);
+    final double paintExtent = maxExtent - constraints.scrollOffset;
+    geometry = new SliverGeometry(
+      scrollExtent: maxExtent,
+      paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
+      maxPaintExtent: maxExtent,
+    );
+    _childPosition = math.min(0.0, paintExtent - childExtent);
+  }
+
+  @override
+  double childPosition(RenderBox child) {
+    assert(child == this.child);
+    return _childPosition;
+  }
+}
+
+/// A sliver with a [RenderBox] child which never scrolls off the viewport in
+/// the positive scroll direction, and which first scrolls on at a full size but
+/// then shrinks as the viewport continues to scroll.
+///
+/// This sliver avoids overlapping other earlier slivers where possible.
+abstract class RenderSliverPinnedAppBar extends RenderSliverAppBar {
+  RenderSliverPinnedAppBar({
+    RenderBox child,
+  }) : super(child: child);
+
+  @override
+  void performLayout() {
+    final double maxExtent = this.maxExtent;
+    layoutChild(constraints.scrollOffset + constraints.overlap, maxExtent);
+    geometry = new SliverGeometry(
+      scrollExtent: maxExtent,
+      paintExtent: math.min(constraints.overlap + childExtent, constraints.remainingPaintExtent),
+      layoutExtent: (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent),
+      maxPaintExtent: constraints.overlap + maxExtent,
+    );
+  }
+
+  @override
+  double childPosition(RenderBox child) {
+    assert(child == this.child);
+    return constraints?.overlap;
+  }
+}
+
+abstract class RenderSliverFloatingAppBar extends RenderSliverAppBar {
+  RenderSliverFloatingAppBar({
+    RenderBox child,
+  }) : super(child: child);
+
+  double _lastActualScrollOffset;
+  double _effectiveScrollOffset;
+
+  // Distance from our leading edge to the child's leading edge, in the axis
+  // direction. Negative if we're scrolled off the top.
+  double _childPosition;
+
+  @override
+  void performLayout() {
+    final double maxExtent = this.maxExtent;
+    if (_lastActualScrollOffset != null && // We've laid out at least once to get an initial position, and either
+        ((constraints.scrollOffset < _lastActualScrollOffset) || // we are scrolling back, so should reveal, or
+         (_effectiveScrollOffset < maxExtent))) { // some part of it is visible, so should shrink or reveal as appropriate.
+      double delta = _lastActualScrollOffset - constraints.scrollOffset;
+      final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward;
+      if (allowFloatingExpansion) {
+        if (_effectiveScrollOffset > maxExtent) // We're scrolled off-screen, but should reveal, so
+          _effectiveScrollOffset = maxExtent; // pretend we're just at the limit.
+      } else {
+        if (delta > 0.0) // If we are trying to expand when allowFloatingExpansion is false,
+          delta = 0.0; // disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.)
+      }
+      _effectiveScrollOffset = (_effectiveScrollOffset - delta).clamp(0.0, constraints.scrollOffset);
+    } else {
+      _effectiveScrollOffset = constraints.scrollOffset;
+    }
+    layoutChild(_effectiveScrollOffset, maxExtent);
+    final double paintExtent = maxExtent - _effectiveScrollOffset;
+    final double layoutExtent = (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);
+    geometry = new SliverGeometry(
+      scrollExtent: maxExtent,
+      paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
+      layoutExtent: layoutExtent,
+      maxPaintExtent: maxExtent,
+    );
+    _childPosition = math.min(0.0, paintExtent - childExtent);
+    _lastActualScrollOffset = constraints.scrollOffset;
+  }
+
+  @override
+  double childPosition(RenderBox child) {
+    assert(child == this.child);
+    return _childPosition;
+  }
+
+  @override
+  void debugFillDescription(List<String> description) {
+    super.debugFillDescription(description);
+    description.add('effective scroll offset: ${_effectiveScrollOffset?.toStringAsFixed(1)}');
+  }
+}
diff --git a/packages/flutter/lib/src/widgets/app_bar.dart b/packages/flutter/lib/src/widgets/app_bar.dart
new file mode 100644
index 0000000..058020f
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/app_bar.dart
@@ -0,0 +1,228 @@
+// 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/foundation.dart';
+import 'package:flutter/rendering.dart';
+
+import 'framework.dart';
+
+abstract class SliverAppBarDelegate {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const SliverAppBarDelegate();
+
+  Widget build(BuildContext context, double shrinkOffset);
+
+  double get maxExtent;
+
+  bool shouldRebuild(@checked SliverAppBarDelegate oldDelegate);
+}
+
+class SliverAppBar extends StatelessWidget {
+  SliverAppBar({
+    Key key,
+    @required this.delegate,
+    this.pinned: false,
+    this.floating: false,
+  }) : super(key: key) {
+    assert(delegate != null);
+    assert(pinned != null);
+    assert(floating != null);
+    assert(!pinned || !floating);
+  }
+
+  final SliverAppBarDelegate delegate;
+
+  final bool pinned;
+
+  final bool floating;
+
+  @override
+  Widget build(BuildContext context) {
+    if (pinned)
+      return new _SliverPinnedAppBar(delegate: delegate);
+    if (floating)
+      return new _SliverFloatingAppBar(delegate: delegate);
+    return new _SliverScrollingAppBar(delegate: delegate);
+  }
+
+  @override
+  void debugFillDescription(List<String> description) {
+    super.debugFillDescription(description);
+    description.add('delegate: $delegate');
+    List<String> flags = <String>[];
+    if (pinned)
+      flags.add('pinned');
+    if (floating)
+      flags.add('floating');
+    if (flags.isEmpty)
+      flags.add('normal');
+    description.add('mode: ${flags.join(", ")}');
+  }
+}
+
+class _SliverAppBarElement extends RenderObjectElement {
+  _SliverAppBarElement(_SliverAppBarRenderObjectWidget widget) : super(widget);
+
+  @override
+  _SliverAppBarRenderObjectWidget get widget => super.widget;
+
+  @override
+  _RenderSliverAppBarForWidgetsMixin get renderObject => super.renderObject;
+
+  @override
+  void mount(Element parent, dynamic newSlot) {
+    super.mount(parent, newSlot);
+    renderObject._element = this;
+  }
+
+  @override
+  void unmount() {
+    super.unmount();
+    renderObject._element = null;
+  }
+
+  @override
+  void update(_SliverAppBarRenderObjectWidget newWidget) {
+    final _SliverAppBarRenderObjectWidget oldWidget = widget;
+    super.update(newWidget);
+    final SliverAppBarDelegate newDelegate = newWidget.delegate;
+    final SliverAppBarDelegate oldDelegate = oldWidget.delegate;
+    if (newDelegate != oldDelegate &&
+        (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate)))
+      renderObject.triggerRebuild();
+  }
+
+  @override
+  void performRebuild() {
+    renderObject.triggerRebuild();
+  }
+
+  Element child;
+
+  void _build(double shrinkOffset) {
+    owner.buildScope(this, () {
+      child = updateChild(child, widget.delegate.build(this, shrinkOffset), null);
+    });
+  }
+
+  @override
+  void forgetChild(Element child) {
+    assert(child == this.child);
+    this.child = null;
+  }
+
+  @override
+  void insertChildRenderObject(@checked RenderObject child, Null slot) {
+    renderObject.child = child;
+  }
+
+  @override
+  void moveChildRenderObject(@checked RenderObject child, Null slot) {
+    assert(false);
+  }
+
+  @override
+  void removeChildRenderObject(@checked RenderObject child) {
+    renderObject.child = null;
+  }
+
+  @override
+  void visitChildren(ElementVisitor visitor) {
+    visitor(child);
+  }
+}
+
+abstract class _SliverAppBarRenderObjectWidget extends RenderObjectWidget {
+  _SliverAppBarRenderObjectWidget({
+    Key key,
+    @required this.delegate,
+  }) : super(key: key) {
+    assert(delegate != null);
+  }
+
+  final SliverAppBarDelegate delegate;
+
+  @override
+  _SliverAppBarElement createElement() => new _SliverAppBarElement(this);
+
+  @override
+  _RenderSliverAppBarForWidgetsMixin createRenderObject(BuildContext context);
+
+  @override
+  void debugFillDescription(List<String> description) {
+    super.debugFillDescription(description);
+    description.add('delegate: $delegate');
+  }
+}
+
+abstract class _RenderSliverAppBarForWidgetsMixin implements RenderSliverAppBar {
+  _SliverAppBarElement _element;
+
+  @override
+  double get maxExtent => _element.widget.delegate.maxExtent;
+
+  @override
+  void updateChild(double shrinkOffset) {
+    assert(_element != null);
+    _element._build(shrinkOffset);
+  }
+
+  @protected
+  void triggerRebuild() {
+    markNeedsUpdate();
+  }
+}
+
+class _SliverScrollingAppBar extends _SliverAppBarRenderObjectWidget {
+  _SliverScrollingAppBar({
+    Key key,
+    @required SliverAppBarDelegate delegate,
+  }) : super(key: key, delegate: delegate);
+
+  @override
+  _RenderSliverAppBarForWidgetsMixin createRenderObject(BuildContext context) {
+    return new _RenderSliverScrollingAppBarForWidgets();
+  }
+}
+
+// This class exists to work around https://github.com/dart-lang/sdk/issues/15101
+abstract class _RenderSliverScrollingAppBar extends RenderSliverScrollingAppBar { }
+
+class _RenderSliverScrollingAppBarForWidgets extends _RenderSliverScrollingAppBar
+  with _RenderSliverAppBarForWidgetsMixin { }
+
+class _SliverPinnedAppBar extends _SliverAppBarRenderObjectWidget {
+  _SliverPinnedAppBar({
+    Key key,
+    @required SliverAppBarDelegate delegate,
+  }) : super(key: key, delegate: delegate);
+
+  @override
+  _RenderSliverAppBarForWidgetsMixin createRenderObject(BuildContext context) {
+    return new _RenderSliverPinnedAppBarForWidgets();
+  }
+}
+
+// This class exists to work around https://github.com/dart-lang/sdk/issues/15101
+abstract class _RenderSliverPinnedAppBar extends RenderSliverPinnedAppBar { }
+
+class _RenderSliverPinnedAppBarForWidgets extends _RenderSliverPinnedAppBar with _RenderSliverAppBarForWidgetsMixin { }
+
+class _SliverFloatingAppBar extends _SliverAppBarRenderObjectWidget {
+  _SliverFloatingAppBar({
+    Key key,
+    @required SliverAppBarDelegate delegate,
+  }) : super(key: key, delegate: delegate);
+
+  @override
+  _RenderSliverAppBarForWidgetsMixin createRenderObject(BuildContext context) {
+    return new _RenderSliverFloatingAppBarForWidgets();
+  }
+}
+
+// This class exists to work around https://github.com/dart-lang/sdk/issues/15101
+abstract class _RenderSliverFloatingAppBar extends RenderSliverFloatingAppBar { }
+
+class _RenderSliverFloatingAppBarForWidgets extends _RenderSliverFloatingAppBar with _RenderSliverAppBarForWidgetsMixin { }
diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart
index 39599e9..2d33d4f 100644
--- a/packages/flutter/lib/widgets.dart
+++ b/packages/flutter/lib/widgets.dart
@@ -10,6 +10,7 @@
 export 'src/widgets/animated_cross_fade.dart';
 export 'src/widgets/animated_size.dart';
 export 'src/widgets/app.dart';
+export 'src/widgets/app_bar.dart';
 export 'src/widgets/banner.dart';
 export 'src/widgets/basic.dart';
 export 'src/widgets/binding.dart';
diff --git a/packages/flutter/test/widgets/slivers_appbar_floating_test.dart b/packages/flutter/test/widgets/slivers_appbar_floating_test.dart
new file mode 100644
index 0000000..ca8b9db
--- /dev/null
+++ b/packages/flutter/test/widgets/slivers_appbar_floating_test.dart
@@ -0,0 +1,251 @@
+// Copyright 2016 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 verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
+  RenderSliver target = key.currentContext.findRenderObject();
+  expect(target.parent, new isInstanceOf<RenderViewport2>());
+  SliverPhysicalParentData parentData = target.parentData;
+  Offset actual = parentData.paintOffset;
+  expect(actual, ideal);
+  SliverGeometry geometry = target.geometry;
+  expect(geometry.visible, visible);
+}
+
+void verifyActualBoxPosition(WidgetTester tester, Finder finder, int index, Rect ideal) {
+  RenderBox box = tester.renderObjectList<RenderBox>(finder).elementAt(index);
+  Rect rect = new Rect.fromPoints(box.localToGlobal(Point.origin), box.localToGlobal(box.size.bottomRight(Point.origin)));
+  expect(rect, equals(ideal));
+}
+
+void main() {
+  testWidgets('Sliver appbars - floating - scroll offset doesn\'t change', (WidgetTester tester) async {
+    final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
+    const double bigHeight = 1000.0;
+    await tester.pumpWidget(
+      new Scrollable2(
+        key: scrollableKey,
+        axisDirection: AxisDirection.down,
+        children: <Widget>[
+          new BigSliver(height: bigHeight),
+          new SliverAppBar(delegate: new TestDelegate(), floating: true),
+          new BigSliver(height: bigHeight),
+        ],
+      ),
+    );
+    AbsoluteScrollPosition position = scrollableKey.currentState.position;
+    final double max = bigHeight * 2.0 + new TestDelegate().maxExtent - 600.0; // 600 is the height of the test viewport
+    assert(max < 10000.0);
+    expect(max, 1600.0);
+    expect(position.pixels, 0.0);
+    expect(position.minScrollExtent, 0.0);
+    expect(position.maxScrollExtent, max);
+    position.animate(to: 10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 10));
+    expect(position.pixels, max);
+    expect(position.minScrollExtent, 0.0);
+    expect(position.maxScrollExtent, max);
+  });
+
+  testWidgets('Sliver appbars - floating - normal behavior works', (WidgetTester tester) async {
+    final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
+    final TestDelegate delegate = new TestDelegate();
+    const double bigHeight = 1000.0;
+    GlobalKey key1, key2, key3;
+    await tester.pumpWidget(
+      new Scrollable2(
+        key: scrollableKey,
+        axisDirection: AxisDirection.down,
+        children: <Widget>[
+          new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
+          new SliverAppBar(key: key2 = new GlobalKey(), delegate: delegate, floating: true),
+          new BigSliver(key: key3 = new GlobalKey(), height: bigHeight),
+        ],
+      ),
+    );
+    AbsoluteScrollPosition position = scrollableKey.currentState.position;
+
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key2, new Offset(0.0, 600.0), false);
+    verifyPaintPosition(key3, new Offset(0.0, 600.0), false);
+
+    position.animate(to: bigHeight - 600.0 + delegate.maxExtent, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key2, new Offset(0.0, 600.0 - delegate.maxExtent), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 0, new Rect.fromLTWH(0.0, 600.0 - delegate.maxExtent, 800.0, delegate.maxExtent));
+    verifyPaintPosition(key3, new Offset(0.0, 600.0), false);
+
+    assert(delegate.maxExtent * 2.0 < 600.0); // make sure this fits on the test screen...
+    position.animate(to: bigHeight - 600.0 + delegate.maxExtent * 2.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key2, new Offset(0.0, 600.0 - delegate.maxExtent * 2.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 0, new Rect.fromLTWH(0.0, 600.0 - delegate.maxExtent * 2.0, 800.0, delegate.maxExtent));
+    verifyPaintPosition(key3, new Offset(0.0, 600.0 - delegate.maxExtent), true);
+
+    position.animate(to: bigHeight, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 0, new Rect.fromLTWH(0.0, 0.0, 800.0, delegate.maxExtent));
+    verifyPaintPosition(key3, new Offset(0.0, delegate.maxExtent), true);
+
+    position.animate(to: bigHeight + delegate.maxExtent * 0.1, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 0, new Rect.fromLTWH(0.0, 0.0, 800.0, delegate.maxExtent * 0.9));
+    verifyPaintPosition(key3, new Offset(0.0, delegate.maxExtent * 0.9), true);
+
+    position.animate(to: bigHeight + delegate.maxExtent * 0.5, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 0, new Rect.fromLTWH(0.0, 0.0, 800.0, delegate.maxExtent * 0.5));
+    verifyPaintPosition(key3, new Offset(0.0, delegate.maxExtent * 0.5), true);
+
+    position.animate(to: bigHeight + delegate.maxExtent * 0.9, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 0, new Rect.fromLTWH(0.0, -delegate.maxExtent * 0.4, 800.0, delegate.maxExtent * 0.5));
+    verifyPaintPosition(key3, new Offset(0.0, delegate.maxExtent * 0.1), true);
+
+    position.animate(to: bigHeight + delegate.maxExtent * 2.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+  });
+
+  testWidgets('Sliver appbars - floating - no floating behavior when animating', (WidgetTester tester) async {
+    final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
+    final TestDelegate delegate = new TestDelegate();
+    const double bigHeight = 1000.0;
+    GlobalKey key1, key2, key3;
+    await tester.pumpWidget(
+      new Scrollable2(
+        key: scrollableKey,
+        axisDirection: AxisDirection.down,
+        children: <Widget>[
+          new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
+          new SliverAppBar(key: key2 = new GlobalKey(), delegate: delegate, floating: true),
+          new BigSliver(key: key3 = new GlobalKey(), height: bigHeight),
+        ],
+      ),
+    );
+    AbsoluteScrollPosition position = scrollableKey.currentState.position;
+
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key2, new Offset(0.0, 600.0), false);
+    verifyPaintPosition(key3, new Offset(0.0, 600.0), false);
+
+    position.animate(to: bigHeight + delegate.maxExtent * 2.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+
+    position.animate(to: bigHeight + delegate.maxExtent * 1.9, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+  });
+
+  testWidgets('Sliver appbars - floating - floating behavior when dragging down', (WidgetTester tester) async {
+    final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
+    final TestDelegate delegate = new TestDelegate();
+    const double bigHeight = 1000.0;
+    GlobalKey key1, key2, key3;
+    await tester.pumpWidget(
+      new Scrollable2(
+        key: scrollableKey,
+        axisDirection: AxisDirection.down,
+        children: <Widget>[
+          new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
+          new SliverAppBar(key: key2 = new GlobalKey(), delegate: delegate, floating: true),
+          new BigSliver(key: key3 = new GlobalKey(), height: bigHeight),
+        ],
+      ),
+    );
+    AbsoluteScrollPosition position = scrollableKey.currentState.position;
+
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key2, new Offset(0.0, 600.0), false);
+    verifyPaintPosition(key3, new Offset(0.0, 600.0), false);
+
+    position.animate(to: bigHeight + delegate.maxExtent * 2.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+
+    position.animate(to: bigHeight + delegate.maxExtent * 1.9, curve: Curves.linear, duration: const Duration(minutes: 1));
+    position.updateUserScrollDirection(ScrollDirection.forward); // ignore: INVALID_USE_OF_PROTECTED_MEMBER, since this is using a protected method for testing purposes
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 0, new Rect.fromLTWH(0.0, -delegate.maxExtent * 0.4, 800.0, delegate.maxExtent * 0.5));
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+  });
+}
+
+class TestDelegate extends SliverAppBarDelegate {
+  @override
+  double get maxExtent => 200.0;
+
+  @override
+  Widget build(BuildContext context, double shrinkOffset) {
+    return new Container(constraints: new BoxConstraints(minHeight: maxExtent / 2.0, maxHeight: maxExtent));
+  }
+
+  @override
+  bool shouldRebuild(TestDelegate oldDelegate) => false;
+}
+
+
+class RenderBigSliver extends RenderSliver {
+  RenderBigSliver(double height) : _height = height;
+
+  double get height => _height;
+  double _height;
+  set height(double value) {
+    if (value == _height)
+      return;
+    _height = value;
+    markNeedsLayout();
+  }
+
+  double get paintExtent => (height - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);
+
+  @override
+  void performLayout() {
+    geometry = new SliverGeometry(
+      scrollExtent: height,
+      paintExtent: paintExtent,
+      maxPaintExtent: height,
+    );
+  }
+}
+
+class BigSliver extends LeafRenderObjectWidget {
+  BigSliver({ Key key, this.height }) : super(key: key);
+
+  final double height;
+
+  @override
+  RenderBigSliver createRenderObject(BuildContext context) {
+    return new RenderBigSliver(height);
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, RenderBigSliver renderObject) {
+    renderObject.height = height;
+  }
+}
diff --git a/packages/flutter/test/widgets/slivers_appbar_pinned_test.dart b/packages/flutter/test/widgets/slivers_appbar_pinned_test.dart
new file mode 100644
index 0000000..8af70e4
--- /dev/null
+++ b/packages/flutter/test/widgets/slivers_appbar_pinned_test.dart
@@ -0,0 +1,243 @@
+// Copyright 2016 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 verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
+  RenderSliver target = key.currentContext.findRenderObject();
+  expect(target.parent, new isInstanceOf<RenderViewport2>());
+  SliverPhysicalParentData parentData = target.parentData;
+  Offset actual = parentData.paintOffset;
+  expect(actual, ideal);
+  SliverGeometry geometry = target.geometry;
+  expect(geometry.visible, visible);
+}
+
+void verifyActualBoxPosition(WidgetTester tester, Finder finder, int index, Rect ideal) {
+  RenderBox box = tester.renderObjectList/*<RenderBox>*/(finder).elementAt(index);
+  Rect rect = new Rect.fromPoints(box.localToGlobal(Point.origin), box.localToGlobal(box.size.bottomRight(Point.origin)));
+  expect(rect, equals(ideal));
+}
+
+void main() {
+  testWidgets('Sliver appbars - pinned', (WidgetTester tester) async {
+    final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
+    const double bigHeight = 550.0;
+    GlobalKey key1, key2, key3, key4, key5;
+    await tester.pumpWidget(
+      new Scrollable2(
+        key: scrollableKey,
+        axisDirection: AxisDirection.down,
+        children: <Widget>[
+          new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
+          new SliverAppBar(key: key2 = new GlobalKey(), delegate: new TestDelegate(), pinned: true),
+          new SliverAppBar(key: key3 = new GlobalKey(), delegate: new TestDelegate(), pinned: true),
+          new BigSliver(key: key4 = new GlobalKey(), height: bigHeight),
+          new BigSliver(key: key5 = new GlobalKey(), height: bigHeight),
+        ],
+      ),
+    );
+    AbsoluteScrollPosition position = scrollableKey.currentState.position;
+    final double max = bigHeight * 3.0 + new TestDelegate().maxExtent * 2.0 - 600.0; // 600 is the height of the test viewport
+    assert(max < 10000.0);
+    expect(max, 1450.0);
+    expect(position.pixels, 0.0);
+    expect(position.minScrollExtent, 0.0);
+    expect(position.maxScrollExtent, max);
+    position.animate(to: 10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 10));
+    expect(position.pixels, max);
+    expect(position.minScrollExtent, 0.0);
+    expect(position.maxScrollExtent, max);
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key4, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 50.0), true);
+  });
+
+  testWidgets('Sliver appbars - pinned with slow scroll', (WidgetTester tester) async {
+    final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
+    const double bigHeight = 550.0;
+    GlobalKey key1, key2, key3, key4, key5;
+    await tester.pumpWidget(
+      new Scrollable2(
+        key: scrollableKey,
+        axisDirection: AxisDirection.down,
+        children: <Widget>[
+          new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
+          new SliverAppBar(key: key2 = new GlobalKey(), delegate: new TestDelegate(), pinned: true),
+          new SliverAppBar(key: key3 = new GlobalKey(), delegate: new TestDelegate(), pinned: true),
+          new BigSliver(key: key4 = new GlobalKey(), height: bigHeight),
+          new BigSliver(key: key5 = new GlobalKey(), height: bigHeight),
+        ],
+      ),
+    );
+    AbsoluteScrollPosition position = scrollableKey.currentState.position;
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key2, new Offset(0.0, 550.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 600.0), false);
+    verifyPaintPosition(key4, new Offset(0.0, 600.0), false);
+    verifyPaintPosition(key5, new Offset(0.0, 600.0), false);
+    position.animate(to: 550.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 100));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 200.0), true);
+    verifyPaintPosition(key4, new Offset(0.0, 400.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 600.0), false);
+    position.animate(to: 600.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 200));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 150.0), true);
+    verifyPaintPosition(key4, new Offset(0.0, 350.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 600.0), false);
+    position.animate(to: 650.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 300));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 100.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 1, new Rect.fromLTWH(0.0, 100.0, 800.0, 200.0));
+    verifyPaintPosition(key4, new Offset(0.0, 300.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 600.0), false);
+    position.animate(to: 700.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 400));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 50.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 1, new Rect.fromLTWH(0.0, 100.0, 800.0, 150.0));
+    verifyPaintPosition(key4, new Offset(0.0, 250.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 600.0), false);
+    position.animate(to: 750.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 500));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 1, new Rect.fromLTWH(0.0, 100.0, 800.0, 100.0));
+    verifyPaintPosition(key4, new Offset(0.0, 200.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 600.0), false);
+    position.animate(to: 800.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 60));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key4, new Offset(0.0, 150.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 600.0), false);
+    position.animate(to: 850.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 70));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key4, new Offset(0.0, 100.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 600.0), false);
+    position.animate(to: 900.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 80));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key4, new Offset(0.0, 50.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 600.0), false);
+    position.animate(to: 950.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 90));
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+    verifyActualBoxPosition(tester, find.byType(Container), 1, new Rect.fromLTWH(0.0, 100.0, 800.0, 100.0));
+    verifyPaintPosition(key4, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key5, new Offset(0.0, 550.0), true);
+  });
+
+  testWidgets('Sliver appbars - pinned with less overlap', (WidgetTester tester) async {
+    final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
+    const double bigHeight = 650.0;
+    GlobalKey key1, key2, key3, key4, key5;
+    await tester.pumpWidget(
+      new Scrollable2(
+        key: scrollableKey,
+        axisDirection: AxisDirection.down,
+        children: <Widget>[
+          new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
+          new SliverAppBar(key: key2 = new GlobalKey(), delegate: new TestDelegate(), pinned: true),
+          new SliverAppBar(key: key3 = new GlobalKey(), delegate: new TestDelegate(), pinned: true),
+          new BigSliver(key: key4 = new GlobalKey(), height: bigHeight),
+          new BigSliver(key: key5 = new GlobalKey(), height: bigHeight),
+        ],
+      ),
+    );
+    AbsoluteScrollPosition position = scrollableKey.currentState.position;
+    final double max = bigHeight * 3.0 + new TestDelegate().maxExtent * 2.0 - 600.0; // 600 is the height of the test viewport
+    assert(max < 10000.0);
+    expect(max, 1750.0);
+    expect(position.pixels, 0.0);
+    expect(position.minScrollExtent, 0.0);
+    expect(position.maxScrollExtent, max);
+    position.animate(to: 10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 10));
+    expect(position.pixels, max);
+    expect(position.minScrollExtent, 0.0);
+    expect(position.maxScrollExtent, max);
+    verifyPaintPosition(key1, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key2, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key3, new Offset(0.0, 0.0), true);
+    verifyPaintPosition(key4, new Offset(0.0, 0.0), false);
+    verifyPaintPosition(key5, new Offset(0.0, 0.0), true);
+  });
+}
+
+class TestDelegate extends SliverAppBarDelegate {
+  @override
+  double get maxExtent => 200.0;
+
+  @override
+  Widget build(BuildContext context, double shrinkOffset) {
+    return new Container(constraints: new BoxConstraints(minHeight: maxExtent / 2.0, maxHeight: maxExtent));
+  }
+
+  @override
+  bool shouldRebuild(TestDelegate oldDelegate) => false;
+}
+
+
+class RenderBigSliver extends RenderSliver {
+  RenderBigSliver(double height) : _height = height;
+
+  double get height => _height;
+  double _height;
+  set height(double value) {
+    if (value == _height)
+      return;
+    _height = value;
+    markNeedsLayout();
+  }
+
+  double get paintExtent => (height - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);
+
+  @override
+  void performLayout() {
+    geometry = new SliverGeometry(
+      scrollExtent: height,
+      paintExtent: paintExtent,
+      maxPaintExtent: height,
+    );
+  }
+}
+
+class BigSliver extends LeafRenderObjectWidget {
+  BigSliver({ Key key, this.height }) : super(key: key);
+
+  final double height;
+
+  @override
+  RenderBigSliver createRenderObject(BuildContext context) {
+    return new RenderBigSliver(height);
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, RenderBigSliver renderObject) {
+    renderObject.height = height;
+  }
+}
diff --git a/packages/flutter/test/widgets/slivers_appbar_scrolling_test.dart b/packages/flutter/test/widgets/slivers_appbar_scrolling_test.dart
new file mode 100644
index 0000000..2fdbfc6
--- /dev/null
+++ b/packages/flutter/test/widgets/slivers_appbar_scrolling_test.dart
@@ -0,0 +1,112 @@
+// Copyright 2016 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 verifyPaintPosition(GlobalKey key, Offset ideal) {
+  RenderObject target = key.currentContext.findRenderObject();
+  expect(target.parent, new isInstanceOf<RenderViewport2>());
+  SliverPhysicalParentData parentData = target.parentData;
+  Offset actual = parentData.paintOffset;
+  expect(actual, ideal);
+}
+
+void main() {
+  testWidgets('Sliver appbars - scrolling', (WidgetTester tester) async {
+    final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
+    GlobalKey key1, key2, key3, key4, key5;
+    await tester.pumpWidget(
+      new Scrollable2(
+        key: scrollableKey,
+        axisDirection: AxisDirection.down,
+        children: <Widget>[
+          new BigSliver(key: key1 = new GlobalKey()),
+          new SliverAppBar(key: key2 = new GlobalKey(), delegate: new TestDelegate()),
+          new SliverAppBar(key: key3 = new GlobalKey(), delegate: new TestDelegate()),
+          new BigSliver(key: key4 = new GlobalKey()),
+          new BigSliver(key: key5 = new GlobalKey()),
+        ],
+      ),
+    );
+    AbsoluteScrollPosition position = scrollableKey.currentState.position;
+    final double max = RenderBigSliver.height * 3.0 + new TestDelegate().maxExtent * 2.0 - 600.0; // 600 is the height of the test viewport
+    assert(max < 10000.0);
+    expect(max, 1450.0);
+    expect(position.pixels, 0.0);
+    expect(position.minScrollExtent, 0.0);
+    expect(position.maxScrollExtent, max);
+    position.animate(to: 10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 10));
+    expect(position.pixels, max);
+    expect(position.minScrollExtent, 0.0);
+    expect(position.maxScrollExtent, max);
+    verifyPaintPosition(key1, new Offset(0.0, 0.0));
+    verifyPaintPosition(key2, new Offset(0.0, 0.0));
+    verifyPaintPosition(key3, new Offset(0.0, 0.0));
+    verifyPaintPosition(key4, new Offset(0.0, 0.0));
+    verifyPaintPosition(key5, new Offset(0.0, 50.0));
+  });
+
+  testWidgets('Sliver appbars - scrolling off screen', (WidgetTester tester) async {
+    final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
+    GlobalKey key = new GlobalKey();
+    TestDelegate delegate = new TestDelegate();
+    await tester.pumpWidget(
+      new Scrollable2(
+        key: scrollableKey,
+        axisDirection: AxisDirection.down,
+        children: <Widget>[
+          new BigSliver(),
+          new SliverAppBar(key: key, delegate: delegate),
+          new BigSliver(),
+          new BigSliver(),
+        ],
+      ),
+    );
+    AbsoluteScrollPosition position = scrollableKey.currentState.position;
+    position.animate(to: RenderBigSliver.height + delegate.maxExtent - 5.0, curve: Curves.linear, duration: const Duration(minutes: 1));
+    await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
+    RenderBox box = tester.renderObject/*<RenderBox>*/(find.byType(Container));
+    Rect rect = new Rect.fromPoints(box.localToGlobal(Point.origin), box.localToGlobal(box.size.bottomRight(Point.origin)));
+    expect(rect, equals(new Rect.fromLTWH(0.0, -195.0, 800.0, 200.0)));
+  });
+}
+
+class TestDelegate extends SliverAppBarDelegate {
+  @override
+  double get maxExtent => 200.0;
+
+  @override
+  Widget build(BuildContext context, double shrinkOffset) {
+    return new Container(height: maxExtent);
+  }
+
+  @override
+  bool shouldRebuild(TestDelegate oldDelegate) => false;
+}
+
+
+class RenderBigSliver extends RenderSliver {
+  static const double height = 550.0;
+  double get paintExtent => (height - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);
+
+  @override
+  void performLayout() {
+    geometry = new SliverGeometry(
+      scrollExtent: height,
+      paintExtent: paintExtent,
+      maxPaintExtent: height,
+    );
+  }
+}
+
+class BigSliver extends LeafRenderObjectWidget {
+  BigSliver({ Key key }) : super(key: key);
+  @override
+  RenderBigSliver createRenderObject(BuildContext context) {
+    return new RenderBigSliver();
+  }
+}