Skip to content

Commit f3de9a0

Browse files
gnpricechrisbobbe
authored andcommitted
sticky_header: Fix hit-testing of header
Our `hitTestChildren` implementation wasn't using `hitTestBoxChild` correctly: it was attempting to do the job of `childMainAxisPosition`, but `hitTestBoxChild` calls `childMainAxisPosition` for itself. The implementation of `childMainAxisPosition` wasn't correct either. Fix both, and add some tests; sticky_header hadn't had any tests on its hit-test behavior. Fixes: #327
1 parent 04e78dc commit f3de9a0

File tree

2 files changed

+46
-12
lines changed

2 files changed

+46
-12
lines changed

Diff for: lib/widgets/sticky_header.dart

+11-5
Original file line numberDiff line numberDiff line change
@@ -592,11 +592,8 @@ class _RenderSliverStickyHeaderList extends RenderSliver with RenderSliverHelper
592592
assert(child != null);
593593
assert(geometry!.hitTestExtent > 0.0);
594594
if (header != null) {
595-
final headerParentData = (header!.parentData as SliverPhysicalParentData);
596-
final headerOffset = headerParentData.paintOffset
597-
.inDirection(constraints.axisDirection);
598595
if (hitTestBoxChild(BoxHitTestResult.wrap(result), header!,
599-
mainAxisPosition: mainAxisPosition - headerOffset,
596+
mainAxisPosition: mainAxisPosition,
600597
crossAxisPosition: crossAxisPosition)) {
601598
return true;
602599
}
@@ -609,8 +606,17 @@ class _RenderSliverStickyHeaderList extends RenderSliver with RenderSliverHelper
609606
double childMainAxisPosition(RenderObject child) {
610607
if (child == this.child) return 0.0;
611608
assert(child == header);
609+
// We use Sliver*Physical*ParentData, so the header's position is stored in
610+
// physical coordinates. To meet the spec of `childMainAxisPosition`, we
611+
// need to convert to the sliver's coordinate system.
612612
final headerParentData = (header!.parentData as SliverPhysicalParentData);
613-
return headerParentData.paintOffset.inDirection(constraints.axisDirection);
613+
final paintOffset = headerParentData.paintOffset;
614+
return switch (constraints.axisDirection) {
615+
AxisDirection.right => paintOffset.dx,
616+
AxisDirection.left => geometry!.layoutExtent - header!.size.width - paintOffset.dx,
617+
AxisDirection.down => paintOffset.dy,
618+
AxisDirection.up => geometry!.layoutExtent - header!.size.height - paintOffset.dy,
619+
};
614620
}
615621

616622
@override

Diff for: test/widgets/sticky_header_test.dart

+35-7
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,17 @@ Future<void> _checkSequence(
132132
header: _Header(i, height: 20),
133133
child: _Item(i, height: 100))))));
134134

135-
final extent = tester.getSize(find.byType(StickyHeaderListView)).onAxis(axis);
135+
final overallSize = tester.getSize(find.byType(StickyHeaderListView));
136+
final extent = overallSize.onAxis(axis);
136137
assert(extent % 100 == 0);
137138

139+
// A position `inset` from the center of the edge the header is found on.
140+
Offset headerInset(double inset) {
141+
return overallSize.center(Offset.zero)
142+
+ offsetInDirection(axis.coordinateDirection,
143+
(extent / 2 - inset) * (headerAtCoordinateEnd ? 1 : -1));
144+
}
145+
138146
final first = !(reverse ^ reverseHeader);
139147

140148
final itemFinder = first ? find.byType(_Item).first : find.byType(_Item).last;
@@ -145,28 +153,39 @@ Future<void> _checkSequence(
145153
: tester.getBottomRight(finder).inDirection(axis.coordinateDirection);
146154
}
147155

148-
void checkState() {
156+
Future<void> checkState() async {
157+
// Check the header comes from the expected item.
149158
final scrollOffset = controller.position.pixels;
150159
final expectedHeaderIndex = first
151160
? (scrollOffset / 100).floor()
152161
: (extent ~/ 100 - 1) + (scrollOffset / 100).ceil();
153162
check(tester.widget<_Item>(itemFinder).index).equals(expectedHeaderIndex);
154163
check(_headerIndex(tester)).equals(expectedHeaderIndex);
155164

165+
// Check the layout of the header and item.
156166
final expectedItemInsetExtent =
157167
100 - (first ? scrollOffset % 100 : (-scrollOffset) % 100);
168+
final double expectedHeaderInsetExtent =
169+
allowOverflow ? 20 : math.min(20, expectedItemInsetExtent);
158170
check(insetExtent(itemFinder)).equals(expectedItemInsetExtent);
159-
check(insetExtent(find.byType(_Header))).equals(
160-
allowOverflow ? 20 : math.min(20, expectedItemInsetExtent));
171+
check(insetExtent(find.byType(_Header))).equals(expectedHeaderInsetExtent);
172+
173+
// Check the header gets hit when it should, and not when it shouldn't.
174+
await tester.tapAt(headerInset(1));
175+
await tester.tapAt(headerInset(expectedHeaderInsetExtent - 1));
176+
check(_Header.takeTapCount()).equals(2);
177+
await tester.tapAt(headerInset(extent - 1));
178+
await tester.tapAt(headerInset(extent - (expectedHeaderInsetExtent - 1)));
179+
check(_Header.takeTapCount()).equals(0);
161180
}
162181

163182
Future<void> jumpAndCheck(double position) async {
164183
controller.jumpTo(position);
165184
await tester.pump();
166-
checkState();
185+
await checkState();
167186
}
168187

169-
checkState();
188+
await checkState();
170189
await jumpAndCheck(5);
171190
await jumpAndCheck(10);
172191
await jumpAndCheck(20);
@@ -210,12 +229,21 @@ class _Header extends StatelessWidget {
210229
final int index;
211230
final double height;
212231

232+
static int takeTapCount() {
233+
final result = _tapCount;
234+
_tapCount = 0;
235+
return result;
236+
}
237+
static int _tapCount = 0;
238+
213239
@override
214240
Widget build(BuildContext context) {
215241
return SizedBox(
216242
height: height,
217243
width: height, // TODO clean up
218-
child: Text("Header $index"));
244+
child: GestureDetector(
245+
onTap: () => _tapCount++,
246+
child: Text("Header $index")));
219247
}
220248
}
221249

0 commit comments

Comments
 (0)