SliverMainAxisGroup multiple PinnedHeaderSliver children (#163528)
Fixes: #155758 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
7535cb10bc
commit
4e39d13a6f
@ -6,7 +6,9 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:vector_math/vector_math_64.dart';
|
import 'package:vector_math/vector_math_64.dart';
|
||||||
|
|
||||||
import 'object.dart';
|
import 'object.dart';
|
||||||
@ -265,39 +267,46 @@ class RenderSliverMainAxisGroup extends RenderSliver
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
double offset = 0;
|
double scrollOffset = 0;
|
||||||
|
double layoutOffset = 0;
|
||||||
double maxPaintExtent = 0;
|
double maxPaintExtent = 0;
|
||||||
|
double paintOffset = constraints.overlap;
|
||||||
|
|
||||||
RenderSliver? child = firstChild;
|
RenderSliver? child = firstChild;
|
||||||
|
|
||||||
while (child != null) {
|
while (child != null) {
|
||||||
final double beforeOffsetPaintExtent = calculatePaintOffset(
|
final double beforeOffsetPaintExtent = calculatePaintOffset(
|
||||||
constraints,
|
constraints,
|
||||||
from: 0.0,
|
from: 0.0,
|
||||||
to: offset,
|
to: scrollOffset,
|
||||||
);
|
);
|
||||||
child.layout(
|
child.layout(
|
||||||
constraints.copyWith(
|
constraints.copyWith(
|
||||||
scrollOffset: math.max(0.0, constraints.scrollOffset - offset),
|
scrollOffset: math.max(0.0, constraints.scrollOffset - scrollOffset),
|
||||||
cacheOrigin: math.min(0.0, constraints.cacheOrigin + offset),
|
cacheOrigin: math.min(0.0, constraints.cacheOrigin + scrollOffset),
|
||||||
overlap: math.max(0.0, constraints.overlap - beforeOffsetPaintExtent),
|
overlap: math.max(0.0, _fixPrecisionError(paintOffset - beforeOffsetPaintExtent)),
|
||||||
remainingPaintExtent: constraints.remainingPaintExtent - beforeOffsetPaintExtent,
|
remainingPaintExtent: _fixPrecisionError(
|
||||||
remainingCacheExtent:
|
constraints.remainingPaintExtent - beforeOffsetPaintExtent,
|
||||||
constraints.remainingCacheExtent -
|
),
|
||||||
calculateCacheOffset(constraints, from: 0.0, to: offset),
|
remainingCacheExtent: _fixPrecisionError(
|
||||||
precedingScrollExtent: offset + constraints.precedingScrollExtent,
|
constraints.remainingCacheExtent -
|
||||||
|
calculateCacheOffset(constraints, from: 0.0, to: scrollOffset),
|
||||||
|
),
|
||||||
|
precedingScrollExtent: scrollOffset + constraints.precedingScrollExtent,
|
||||||
),
|
),
|
||||||
parentUsesSize: true,
|
parentUsesSize: true,
|
||||||
);
|
);
|
||||||
final SliverGeometry childLayoutGeometry = child.geometry!;
|
final SliverGeometry childLayoutGeometry = child.geometry!;
|
||||||
|
final double childPaintOffset = layoutOffset + childLayoutGeometry.paintOrigin;
|
||||||
final SliverPhysicalParentData childParentData =
|
final SliverPhysicalParentData childParentData =
|
||||||
child.parentData! as SliverPhysicalParentData;
|
child.parentData! as SliverPhysicalParentData;
|
||||||
childParentData.paintOffset = switch (constraints.axis) {
|
childParentData.paintOffset = switch (constraints.axis) {
|
||||||
Axis.vertical => Offset(0.0, beforeOffsetPaintExtent),
|
Axis.vertical => Offset(0.0, childPaintOffset),
|
||||||
Axis.horizontal => Offset(beforeOffsetPaintExtent, 0.0),
|
Axis.horizontal => Offset(childPaintOffset, 0.0),
|
||||||
};
|
};
|
||||||
offset += childLayoutGeometry.scrollExtent;
|
scrollOffset += childLayoutGeometry.scrollExtent;
|
||||||
maxPaintExtent += child.geometry!.maxPaintExtent;
|
layoutOffset += childLayoutGeometry.layoutExtent;
|
||||||
|
maxPaintExtent += childLayoutGeometry.maxPaintExtent;
|
||||||
|
paintOffset = math.max(childPaintOffset + childLayoutGeometry.paintExtent, paintOffset);
|
||||||
child = childAfter(child);
|
child = childAfter(child);
|
||||||
assert(() {
|
assert(() {
|
||||||
if (child != null && maxPaintExtent.isInfinite) {
|
if (child != null && maxPaintExtent.isInfinite) {
|
||||||
@ -310,48 +319,41 @@ class RenderSliverMainAxisGroup extends RenderSliver
|
|||||||
}());
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
final double totalScrollExtent = offset;
|
final double remainingExtent = math.max(0, scrollOffset - constraints.scrollOffset);
|
||||||
offset = 0.0;
|
// If the children's paint extent exceeds the remaining scroll extent of the `RenderSliverMainAxisGroup`,
|
||||||
child = firstChild;
|
// they need to be corrected.
|
||||||
// Second pass to correct out of bound paintOffsets.
|
if (paintOffset > remainingExtent) {
|
||||||
while (child != null) {
|
final double paintCorrection = paintOffset - remainingExtent;
|
||||||
final double beforeOffsetPaintExtent = calculatePaintOffset(
|
paintOffset = remainingExtent;
|
||||||
constraints,
|
child = firstChild;
|
||||||
from: 0.0,
|
while (child != null) {
|
||||||
to: offset,
|
final SliverGeometry childLayoutGeometry = child.geometry!;
|
||||||
);
|
if (childLayoutGeometry.paintExtent > 0) {
|
||||||
final SliverGeometry childLayoutGeometry = child.geometry!;
|
final SliverPhysicalParentData childParentData =
|
||||||
final SliverPhysicalParentData childParentData =
|
child.parentData! as SliverPhysicalParentData;
|
||||||
child.parentData! as SliverPhysicalParentData;
|
childParentData.paintOffset = switch (constraints.axis) {
|
||||||
final double remainingExtent = totalScrollExtent - constraints.scrollOffset;
|
Axis.vertical => Offset(0.0, childParentData.paintOffset.dy - paintCorrection),
|
||||||
if (childLayoutGeometry.paintExtent > remainingExtent) {
|
Axis.horizontal => Offset(childParentData.paintOffset.dx - paintCorrection, 0.0),
|
||||||
final double paintCorrection = childLayoutGeometry.paintExtent - remainingExtent;
|
};
|
||||||
childParentData.paintOffset = switch (constraints.axis) {
|
}
|
||||||
Axis.vertical => Offset(0.0, beforeOffsetPaintExtent - paintCorrection),
|
child = childAfter(child);
|
||||||
Axis.horizontal => Offset(beforeOffsetPaintExtent - paintCorrection, 0.0),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
offset += child.geometry!.scrollExtent;
|
|
||||||
child = childAfter(child);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final double paintExtent = calculatePaintOffset(
|
|
||||||
constraints,
|
|
||||||
from: math.min(constraints.scrollOffset, 0),
|
|
||||||
to: totalScrollExtent,
|
|
||||||
);
|
|
||||||
final double cacheExtent = calculateCacheOffset(
|
final double cacheExtent = calculateCacheOffset(
|
||||||
constraints,
|
constraints,
|
||||||
from: math.min(constraints.scrollOffset, 0),
|
from: math.min(constraints.scrollOffset, 0),
|
||||||
to: totalScrollExtent,
|
to: scrollOffset,
|
||||||
);
|
);
|
||||||
|
final double paintExtent = clampDouble(paintOffset, 0, constraints.remainingPaintExtent);
|
||||||
|
|
||||||
geometry = SliverGeometry(
|
geometry = SliverGeometry(
|
||||||
scrollExtent: totalScrollExtent,
|
scrollExtent: scrollOffset,
|
||||||
paintExtent: paintExtent,
|
paintExtent: paintExtent,
|
||||||
cacheExtent: cacheExtent,
|
cacheExtent: cacheExtent,
|
||||||
maxPaintExtent: maxPaintExtent,
|
maxPaintExtent: maxPaintExtent,
|
||||||
hasVisualOverflow:
|
hasVisualOverflow:
|
||||||
totalScrollExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
|
scrollOffset > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update the children's paintOffset based on the direction again, which
|
// Update the children's paintOffset based on the direction again, which
|
||||||
@ -428,4 +430,8 @@ class RenderSliverMainAxisGroup extends RenderSliver
|
|||||||
child = childAfter(child);
|
child = childAfter(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double _fixPrecisionError(double number) {
|
||||||
|
return number.abs() < precisionErrorTolerance ? 0.0 : number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -955,6 +955,90 @@ void main() {
|
|||||||
expect(renderBox.localToGlobal(Offset.zero), const Offset(0.0, 310.0));
|
expect(renderBox.localToGlobal(Offset.zero), const Offset(0.0, 310.0));
|
||||||
expect(tester.getTopLeft(find.text('1')), const Offset(0.0, 310.0));
|
expect(tester.getTopLeft(find.text('1')), const Offset(0.0, 310.0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('SliverMainAxisGroup multiple PinnedHeaderSliver children', (
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final Size screenSize = tester.view.physicalSize / tester.view.devicePixelRatio;
|
||||||
|
final ScrollController controller = ScrollController();
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
Future<void> pumpWidget({Axis scrollDirection = Axis.vertical, bool reverse = false}) async {
|
||||||
|
Widget buildExtentBox(double size, {Widget? child}) {
|
||||||
|
return switch (scrollDirection) {
|
||||||
|
Axis.vertical => SizedBox(height: size, child: child),
|
||||||
|
Axis.horizontal => SizedBox(width: size, child: child),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_buildSliverMainAxisGroup(
|
||||||
|
controller: controller,
|
||||||
|
viewportHeight: screenSize.height,
|
||||||
|
viewportWidth: screenSize.width,
|
||||||
|
scrollDirection: scrollDirection,
|
||||||
|
reverse: reverse,
|
||||||
|
slivers: <Widget>[
|
||||||
|
PinnedHeaderSliver(child: buildExtentBox(30)),
|
||||||
|
SliverToBoxAdapter(child: buildExtentBox(30)),
|
||||||
|
PinnedHeaderSliver(child: buildExtentBox(20, child: const Text('1'))),
|
||||||
|
SliverToBoxAdapter(child: buildExtentBox(1000)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await pumpWidget();
|
||||||
|
controller.jumpTo(500);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getTopLeft(find.text('1')), const Offset(0, 30));
|
||||||
|
|
||||||
|
await pumpWidget(scrollDirection: Axis.horizontal);
|
||||||
|
controller.jumpTo(500);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getTopLeft(find.text('1')), const Offset(30, 0));
|
||||||
|
|
||||||
|
await pumpWidget(reverse: true);
|
||||||
|
controller.jumpTo(500);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getTopLeft(find.text('1')), Offset(0, screenSize.height - 50));
|
||||||
|
|
||||||
|
await pumpWidget(scrollDirection: Axis.horizontal, reverse: true);
|
||||||
|
controller.jumpTo(500);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getTopLeft(find.text('1')), Offset(screenSize.width - 50, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('SliverMainAxisGroup precision error', (WidgetTester tester) async {
|
||||||
|
final ScrollController controller = ScrollController();
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 201,
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: controller,
|
||||||
|
slivers: const <Widget>[
|
||||||
|
SliverMainAxisGroup(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverToBoxAdapter(child: SizedBox(height: 70)),
|
||||||
|
PinnedHeaderSliver(child: SizedBox(height: 70)),
|
||||||
|
SliverToBoxAdapter(child: SizedBox(height: 70)),
|
||||||
|
PinnedHeaderSliver(child: SizedBox(height: 70)),
|
||||||
|
SliverToBoxAdapter(child: SizedBox(height: 70)),
|
||||||
|
PinnedHeaderSliver(child: SizedBox(height: 70)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
controller.jumpTo(60.22678428085297);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSliverList({
|
Widget _buildSliverList({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user