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