Unverified Commit 517f8dc2 authored by matthew-carroll's avatar matthew-carroll Committed by GitHub

Add consumedScrollExtent to SliverConstraints as reported by Viewport (#24551)

parent 305ab1a3
...@@ -99,6 +99,7 @@ class SliverConstraints extends Constraints { ...@@ -99,6 +99,7 @@ class SliverConstraints extends Constraints {
@required this.growthDirection, @required this.growthDirection,
@required this.userScrollDirection, @required this.userScrollDirection,
@required this.scrollOffset, @required this.scrollOffset,
@required this.precedingScrollExtent,
@required this.overlap, @required this.overlap,
@required this.remainingPaintExtent, @required this.remainingPaintExtent,
@required this.crossAxisExtent, @required this.crossAxisExtent,
...@@ -110,6 +111,7 @@ class SliverConstraints extends Constraints { ...@@ -110,6 +111,7 @@ class SliverConstraints extends Constraints {
assert(growthDirection != null), assert(growthDirection != null),
assert(userScrollDirection != null), assert(userScrollDirection != null),
assert(scrollOffset != null), assert(scrollOffset != null),
assert(precedingScrollExtent != null),
assert(overlap != null), assert(overlap != null),
assert(remainingPaintExtent != null), assert(remainingPaintExtent != null),
assert(crossAxisExtent != null), assert(crossAxisExtent != null),
...@@ -125,6 +127,7 @@ class SliverConstraints extends Constraints { ...@@ -125,6 +127,7 @@ class SliverConstraints extends Constraints {
GrowthDirection growthDirection, GrowthDirection growthDirection,
ScrollDirection userScrollDirection, ScrollDirection userScrollDirection,
double scrollOffset, double scrollOffset,
double precedingScrollExtent,
double overlap, double overlap,
double remainingPaintExtent, double remainingPaintExtent,
double crossAxisExtent, double crossAxisExtent,
...@@ -138,6 +141,7 @@ class SliverConstraints extends Constraints { ...@@ -138,6 +141,7 @@ class SliverConstraints extends Constraints {
growthDirection: growthDirection ?? this.growthDirection, growthDirection: growthDirection ?? this.growthDirection,
userScrollDirection: userScrollDirection ?? this.userScrollDirection, userScrollDirection: userScrollDirection ?? this.userScrollDirection,
scrollOffset: scrollOffset ?? this.scrollOffset, scrollOffset: scrollOffset ?? this.scrollOffset,
precedingScrollExtent: precedingScrollExtent ?? this.precedingScrollExtent,
overlap: overlap ?? this.overlap, overlap: overlap ?? this.overlap,
remainingPaintExtent: remainingPaintExtent ?? this.remainingPaintExtent, remainingPaintExtent: remainingPaintExtent ?? this.remainingPaintExtent,
crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent, crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent,
...@@ -225,6 +229,29 @@ class SliverConstraints extends Constraints { ...@@ -225,6 +229,29 @@ class SliverConstraints extends Constraints {
/// contents depends on the [growthDirection]. /// contents depends on the [growthDirection].
final double scrollOffset; final double scrollOffset;
/// The scroll distance that has been consumed by all [Sliver]s that came
/// before this [Sliver].
///
/// # Edge Cases
///
/// [Sliver]s often lazily create their internal content as layout occurs,
/// e.g., [SliverList]. In this case, when [Sliver]s exceed the viewport,
/// their children are built lazily, and the [Sliver] does not have enough
/// information to estimate its total extent, [precedingScrollExtent] will be
/// [double.infinity] for all [Sliver]s that appear after the lazily
/// constructed child. This is because a total [scrollExtent] cannot be
/// calculated unless all inner children have been created and sized, or the
/// number of children and estimated extents are provided. The infinite
/// [scrollExtent] will become finite as soon as enough information is
/// available to estimate the overall extent of all children within the given
/// [Sliver].
///
/// [Sliver]s may legitimately be infinite, meaning that they can scroll
/// content forever without reaching the end. For any [Sliver]s that appear
/// after the infinite [Sliver], the [precedingScrollExtent] will be
/// [double.infinity].
final double precedingScrollExtent;
/// The number of pixels from where the pixels corresponding to the /// The number of pixels from where the pixels corresponding to the
/// [scrollOffset] will be painted up to the first pixel that has not yet been /// [scrollOffset] will be painted up to the first pixel that has not yet been
/// painted on by an earlier sliver, in the [axisDirection]. /// painted on by an earlier sliver, in the [axisDirection].
......
...@@ -388,17 +388,18 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -388,17 +388,18 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
applyGrowthDirectionToScrollDirection(offset.userScrollDirection, growthDirection); applyGrowthDirectionToScrollDirection(offset.userScrollDirection, growthDirection);
assert(adjustedUserScrollDirection != null); assert(adjustedUserScrollDirection != null);
double maxPaintOffset = layoutOffset + overlap; double maxPaintOffset = layoutOffset + overlap;
double precedingScrollExtent = 0.0;
while (child != null) { while (child != null) {
final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset; final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset;
// If the scrollOffset is too small we adjust the paddedOrigin because it // If the scrollOffset is too small we adjust the paddedOrigin because it
// doesn't make sense to ask a sliver for content before its scroll // doesn't make sense to ask a sliver for content before its scroll
// offset. // offset.
final double corectedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset); final double correctedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset);
final double cacheExtentCorrection = cacheOrigin - corectedCacheOrigin; final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;
assert(sliverScrollOffset >= corectedCacheOrigin.abs()); assert(sliverScrollOffset >= correctedCacheOrigin.abs());
assert(corectedCacheOrigin <= 0.0); assert(correctedCacheOrigin <= 0.0);
assert(sliverScrollOffset >= 0.0); assert(sliverScrollOffset >= 0.0);
assert(cacheExtentCorrection <= 0.0); assert(cacheExtentCorrection <= 0.0);
...@@ -407,13 +408,14 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -407,13 +408,14 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
growthDirection: growthDirection, growthDirection: growthDirection,
userScrollDirection: adjustedUserScrollDirection, userScrollDirection: adjustedUserScrollDirection,
scrollOffset: sliverScrollOffset, scrollOffset: sliverScrollOffset,
precedingScrollExtent: precedingScrollExtent,
overlap: maxPaintOffset - layoutOffset, overlap: maxPaintOffset - layoutOffset,
remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset), remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
crossAxisExtent: crossAxisExtent, crossAxisExtent: crossAxisExtent,
crossAxisDirection: crossAxisDirection, crossAxisDirection: crossAxisDirection,
viewportMainAxisExtent: mainAxisExtent, viewportMainAxisExtent: mainAxisExtent,
remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection), remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),
cacheOrigin: corectedCacheOrigin, cacheOrigin: correctedCacheOrigin,
), parentUsesSize: true); ), parentUsesSize: true);
final SliverGeometry childLayoutGeometry = child.geometry; final SliverGeometry childLayoutGeometry = child.geometry;
...@@ -438,10 +440,11 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -438,10 +440,11 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset); maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
scrollOffset -= childLayoutGeometry.scrollExtent; scrollOffset -= childLayoutGeometry.scrollExtent;
precedingScrollExtent += childLayoutGeometry.scrollExtent;
layoutOffset += childLayoutGeometry.layoutExtent; layoutOffset += childLayoutGeometry.layoutExtent;
if (childLayoutGeometry.cacheExtent != 0.0) { if (childLayoutGeometry.cacheExtent != 0.0) {
remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection; remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;
cacheOrigin = math.min(corectedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0); cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
} }
updateOutOfBandData(growthDirection, childLayoutGeometry); updateOutOfBandData(growthDirection, childLayoutGeometry);
......
...@@ -6,22 +6,47 @@ import 'package:flutter/rendering.dart'; ...@@ -6,22 +6,47 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
test('AxisDirection and applyGrowthDirectionToAxisDirection', () { test('applyGrowthDirectionToAxisDirection produces expected AxisDirection', () {
expect(AxisDirection.values.length, 4); expect(AxisDirection.values.length, 4);
for (AxisDirection axisDirection in AxisDirection.values) for (AxisDirection axisDirection in AxisDirection.values) {
expect(applyGrowthDirectionToAxisDirection(axisDirection, GrowthDirection.forward), axisDirection); expect(applyGrowthDirectionToAxisDirection(axisDirection, GrowthDirection.forward), axisDirection);
}
expect(applyGrowthDirectionToAxisDirection(AxisDirection.up, GrowthDirection.reverse), AxisDirection.down); expect(applyGrowthDirectionToAxisDirection(AxisDirection.up, GrowthDirection.reverse), AxisDirection.down);
expect(applyGrowthDirectionToAxisDirection(AxisDirection.down, GrowthDirection.reverse), AxisDirection.up); expect(applyGrowthDirectionToAxisDirection(AxisDirection.down, GrowthDirection.reverse), AxisDirection.up);
expect(applyGrowthDirectionToAxisDirection(AxisDirection.left, GrowthDirection.reverse), AxisDirection.right); expect(applyGrowthDirectionToAxisDirection(AxisDirection.left, GrowthDirection.reverse), AxisDirection.right);
expect(applyGrowthDirectionToAxisDirection(AxisDirection.right, GrowthDirection.reverse), AxisDirection.left); expect(applyGrowthDirectionToAxisDirection(AxisDirection.right, GrowthDirection.reverse), AxisDirection.left);
}); });
test('SliverConstraints', () { test('SliverConstraints are the same when copied', () {
const SliverConstraints original = SliverConstraints(
axisDirection: AxisDirection.down,
growthDirection: GrowthDirection.forward,
userScrollDirection: ScrollDirection.idle,
scrollOffset: 0.0,
precedingScrollExtent: 0.0,
overlap: 0.0,
remainingPaintExtent: 0.0,
crossAxisExtent: 0.0,
crossAxisDirection: AxisDirection.right,
viewportMainAxisExtent: 0.0,
cacheOrigin: 0.0,
remainingCacheExtent: 0.0,
);
final SliverConstraints copy = original.copyWith();
expect(original, equals(copy));
expect(original.hashCode, equals(copy.hashCode));
expect(original.toString(), equals(copy.toString()));
expect(original, hasOneLineDescription);
expect(original.normalizedGrowthDirection, equals(GrowthDirection.forward));
});
test('SliverConstraints normalizedGrowthDirection is inferred from AxisDirection and GrowthDirection', () {
const SliverConstraints a = SliverConstraints( const SliverConstraints a = SliverConstraints(
axisDirection: AxisDirection.down, axisDirection: AxisDirection.down,
growthDirection: GrowthDirection.forward, growthDirection: GrowthDirection.forward,
userScrollDirection: ScrollDirection.idle, userScrollDirection: ScrollDirection.idle,
scrollOffset: 0.0, scrollOffset: 0.0,
precedingScrollExtent: 0.0,
overlap: 0.0, overlap: 0.0,
remainingPaintExtent: 0.0, remainingPaintExtent: 0.0,
crossAxisExtent: 0.0, crossAxisExtent: 0.0,
...@@ -30,12 +55,6 @@ void main() { ...@@ -30,12 +55,6 @@ void main() {
cacheOrigin: 0.0, cacheOrigin: 0.0,
remainingCacheExtent: 0.0, remainingCacheExtent: 0.0,
); );
final SliverConstraints b = a.copyWith();
expect(a, equals(b));
expect(a.hashCode, equals(b.hashCode));
expect(a.toString(), equals(b.toString()));
expect(a, hasOneLineDescription);
expect(a.normalizedGrowthDirection, equals(GrowthDirection.forward));
final SliverConstraints c = a.copyWith( final SliverConstraints c = a.copyWith(
axisDirection: AxisDirection.up, axisDirection: AxisDirection.up,
...@@ -52,6 +71,7 @@ void main() { ...@@ -52,6 +71,7 @@ void main() {
growthDirection: GrowthDirection.reverse, growthDirection: GrowthDirection.reverse,
userScrollDirection: ScrollDirection.forward, userScrollDirection: ScrollDirection.forward,
scrollOffset: 10.0, scrollOffset: 10.0,
precedingScrollExtent: 0.0,
overlap: 20.0, overlap: 20.0,
remainingPaintExtent: 30.0, remainingPaintExtent: 30.0,
crossAxisExtent: 40.0, crossAxisExtent: 40.0,
...@@ -74,11 +94,17 @@ void main() { ...@@ -74,11 +94,17 @@ void main() {
expect(g.normalizedGrowthDirection, equals(GrowthDirection.reverse)); expect(g.normalizedGrowthDirection, equals(GrowthDirection.reverse));
}); });
test('SliverGeometry', () { test('SliverGeometry with no arguments is valid', () {
expect(const SliverGeometry().debugAssertIsValid(), isTrue); expect(const SliverGeometry().debugAssertIsValid(), isTrue);
});
test('SliverGeometry throws error when layoutExtent exceeds paintExtent', () {
expect(() { expect(() {
const SliverGeometry(layoutExtent: 10.0, paintExtent: 9.0).debugAssertIsValid(); const SliverGeometry(layoutExtent: 10.0, paintExtent: 9.0).debugAssertIsValid();
}, throwsFlutterError); }, throwsFlutterError);
});
test('SliverGeometry throws error when maxPaintExtent is less than paintExtent', () {
expect(() { expect(() {
const SliverGeometry(paintExtent: 9.0, maxPaintExtent: 8.0).debugAssertIsValid(); const SliverGeometry(paintExtent: 9.0, maxPaintExtent: 8.0).debugAssertIsValid();
}, throwsFlutterError); }, throwsFlutterError);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
...@@ -749,4 +750,78 @@ void main() { ...@@ -749,4 +750,78 @@ void main() {
expect(sliver.paintBounds, expectedRect); expect(sliver.paintBounds, expectedRect);
expect(sliver.semanticBounds, expectedRect); expect(sliver.semanticBounds, expectedRect);
}); });
test('precedingScrollExtent is 0.0 for first Sliver in list', () {
const double viewportWidth = 800.0;
final RenderSliver sliver = RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(viewportWidth, 150.0)),
);
final RenderViewport root = RenderViewport(
axisDirection: AxisDirection.down,
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
children: <RenderSliver>[
sliver,
],
);
layout(root);
expect(sliver.constraints.precedingScrollExtent, 0.0);
});
test('precedingScrollExtent accumulates over multiple Slivers', () {
const double viewportWidth = 800.0;
final RenderSliver sliver1 = RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(viewportWidth, 150.0)),
);
final RenderSliver sliver2 = RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(viewportWidth, 150.0)),
);
final RenderSliver sliver3 = RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(viewportWidth, 150.0)),
);
final RenderViewport root = RenderViewport(
axisDirection: AxisDirection.down,
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
children: <RenderSliver>[
sliver1,
sliver2,
sliver3,
],
);
layout(root);
// The 3rd Sliver comes after 300.0px of preceding scroll extent by first 2 Slivers.
expect(sliver3.constraints.precedingScrollExtent, 300.0);
});
test('precedingScrollExtent is not impacted by scrollOffset', () {
const double viewportWidth = 800.0;
final RenderSliver sliver1 = RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(viewportWidth, 150.0)),
);
final RenderSliver sliver2 = RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(viewportWidth, 150.0)),
);
final RenderSliver sliver3 = RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(viewportWidth, 150.0)),
);
final RenderViewport root = RenderViewport(
axisDirection: AxisDirection.down,
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.fixed(100.0),
children: <RenderSliver>[
sliver1,
sliver2,
sliver3,
],
);
layout(root);
// The 3rd Sliver comes after 300.0px of preceding scroll extent by first 2 Slivers.
// In this test a ViewportOffset is applied to simulate a scrollOffset. That
// offset is not expected to impact the precedingScrollExtent.
expect(sliver3.constraints.precedingScrollExtent, 300.0);
});
} }
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('precedingScrollExtent is reported as infinity for Sliver of unknown size', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
const SliverToBoxAdapter(child: SizedBox(width: double.infinity, height: 150.0)),
const SliverToBoxAdapter(child: SizedBox(width: double.infinity, height: 150.0)),
SliverList(
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
if (index < 100) {
return const SizedBox(width: double.infinity, height: 150.0);
} else {
return null;
}
}),
),
const SliverToBoxAdapter(
key: Key('final_sliver'),
child: SizedBox(width: double.infinity, height: 150.0),
),
],
),
),
);
// The last Sliver comes after a SliverList that has many more items than
// can fit in the viewport, and the SliverList doesn't report a child count,
// so the SliverList leads to an infinite precedingScrollExtent.
final RenderViewport renderViewport = tester.renderObject(find.byType(Viewport));
final RenderSliver lastRenderSliver = renderViewport.lastChild;
expect(lastRenderSliver.constraints.precedingScrollExtent, double.infinity);
});
}
\ No newline at end of file
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