Skip to content

Commit 4d36175

Browse files
committed
sticky_header: Fix hit-testing when header overflows sliver
When the sticky header overflows the sliver that provides it -- that is, when the sliver boundary is scrolled to within the area the header covers -- the existing code already got the right visual result, painting the header at its full size. But it didn't work properly for hit-testing: trying to tap the header in the portion where it's overflowing wouldn't work, and would instead go through to whatever's underneath (like the top of the next sliver). That's because the geometry it was reporting from this `performLayout` method didn't reflect the geometry it would actually paint in the `paint` method. When hit-testing, that reported geometry gets interpreted by the framework code before calling this render object's other methods. Fix that by reporting an accurate `paintOrigin` and `paintExtent`. After this fix, sticky headers overflowing into the next sliver seem to work completely correctly... as long as the viewport paints the slivers in the necessary order. We'll take care of that next.
1 parent 25b3eee commit 4d36175

File tree

2 files changed

+59
-1
lines changed

2 files changed

+59
-1
lines changed

lib/widgets/sticky_header.dart

+9-1
Original file line numberDiff line numberDiff line change
@@ -617,10 +617,18 @@ class _RenderSliverStickyHeaderList extends RenderSliver with RenderSliverHelper
617617
// even if the (visible part of the) item is smaller than the header,
618618
// and even if the whole child sliver is smaller than the header.
619619

620+
final headerGrowthPlacement =
621+
_widget.headerPlacement._byGrowth(constraints.growthDirection);
622+
620623
geometry = SliverGeometry( // TODO review interaction with other slivers
621624
scrollExtent: geometry.scrollExtent,
622625
layoutExtent: childExtent,
623-
paintExtent: childExtent,
626+
paintExtent: math.max(childExtent, headerExtent),
627+
paintOrigin: switch (headerGrowthPlacement) {
628+
_HeaderGrowthPlacement.growthStart => 0,
629+
_HeaderGrowthPlacement.growthEnd =>
630+
math.min(0.0, childExtent - headerExtent),
631+
},
624632
maxPaintExtent: math.max(geometry.maxPaintExtent, headerExtent),
625633
hasVisualOverflow: geometry.hasVisualOverflow
626634
|| headerExtent > constraints.remainingPaintExtent,

test/widgets/sticky_header_test.dart

+50
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,48 @@ void main() {
220220
await tester.pump();
221221
checkState(103, item: 0, header: 0);
222222
});
223+
224+
testWidgets('hit-testing for header overflowing sliver', (tester) async {
225+
final controller = ScrollController();
226+
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
227+
child: CustomScrollView(
228+
controller: controller,
229+
slivers: [
230+
SliverStickyHeaderList(
231+
headerPlacement: HeaderPlacement.scrollingStart,
232+
delegate: SliverChildListDelegate(
233+
List.generate(100, (i) => StickyHeaderItem(
234+
allowOverflow: true,
235+
header: _Header(i, height: 20),
236+
child: _Item(i, height: 100))))),
237+
SliverStickyHeaderList(
238+
headerPlacement: HeaderPlacement.scrollingStart,
239+
delegate: SliverChildListDelegate(
240+
List.generate(100, (i) => StickyHeaderItem(
241+
allowOverflow: true,
242+
header: _Header(100 + i, height: 20),
243+
child: _Item(100 + i, height: 100))))),
244+
])));
245+
246+
const topExtent = 100 * 100;
247+
for (double topHeight in [5, 10, 15, 20]) {
248+
controller.jumpTo(topExtent - topHeight);
249+
await tester.pump();
250+
// The top sliver occupies height [topHeight].
251+
// Its header overhangs by `20 - topHeight`.
252+
253+
final expected = <Condition<Object?>>[];
254+
for (int y = 1; y < 20; y++) {
255+
await tester.tapAt(Offset(400, y.toDouble()));
256+
expected.add((it) => it.isA<_Header>().index.equals(99));
257+
}
258+
for (int y = 21; y < 40; y += 2) {
259+
await tester.tapAt(Offset(400, y.toDouble()));
260+
expected.add((it) => it.isA<_Item>().index.equals(100));
261+
}
262+
check(_TapLogged.takeTapLog()).deepEquals(expected);
263+
}
264+
});
223265
}
224266

225267
Future<void> _checkSequence(
@@ -389,6 +431,10 @@ class _Header extends StatelessWidget implements _TapLogged {
389431
}
390432
}
391433

434+
extension _HeaderChecks on Subject<_Header> {
435+
Subject<int> get index => has((x) => x.index, 'index');
436+
}
437+
392438
class _Item extends StatelessWidget implements _TapLogged {
393439
const _Item(this.index, {required this.height});
394440

@@ -412,6 +458,10 @@ class _Item extends StatelessWidget implements _TapLogged {
412458
}
413459
}
414460

461+
extension _ItemChecks on Subject<_Item> {
462+
Subject<int> get index => has((x) => x.index, 'index');
463+
}
464+
415465
/// Sets [DeviceGestureSettings.touchSlop] for the child subtree
416466
/// to the given value, by inserting a [MediaQuery].
417467
///

0 commit comments

Comments
 (0)