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:
yim 2025-02-25 13:53:03 +08:00 committed by GitHub
parent 7535cb10bc
commit 4e39d13a6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 135 additions and 45 deletions

View File

@ -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;
}
} }

View File

@ -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({