Commit 1426ef99 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Appbar slivers. (#7631)

parent eba5fb1a
......@@ -45,6 +45,7 @@ export 'src/rendering/rotated_box.dart';
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';
......
// 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)}');
}
}
// 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 { }
......@@ -10,6 +10,7 @@ library widgets;
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';
......
// 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;
}
}
// 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;
}
}
// 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();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment