-
Notifications
You must be signed in to change notification settings - Fork 267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
anchors 3/n: Fix hit-testing when header overflows sliver #1316
Conversation
9ec7683
to
0770222
Compare
Hmm, CI is failing. |
Yeah, in the |
0770222
to
d24f01c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! One comment below about some things I'm not understanding yet.
lib/widgets/sticky_header.dart
Outdated
// Then the [childMainAxisPosition] spec wants a location relative to the | ||
// "leading _visible_ edge" of this sliver; and "visible" means paint, | ||
// not layout. The leading visible edge, expressed again in the sliver's | ||
// layout coordinates, is at `paintOrigin`. | ||
return fromScrollOffset - geometry!.paintOrigin; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having a hard time understanding upstream's SliverGeometry.layoutExtent
. Its dartdoc is written carelessly; it assumes there are multiple slivers—multiple visible slivers:
The distance from the first visible part of this sliver to the first visible part of the next sliver
It's possible that there are multiple slivers (an important case for us), even multiple visible slivers, but I feel a little frustrated that it starts from that as an assumption, and it's not yet easy for me to guess what they really meant.
Then could you please expand on this part a bit?:
"visible" means paint, not layout
Is there an upstream doc that can help clarify this distinction? SliverConstraints.scrollOffset
, SliverPhysicalParentData.paintOffset
, SliverGeometry.layoutExtent
(quoted above), and this RenderSliverMultiBoxAdaptor.childMainAxisPosition
all use this word "visible" and I don't yet see this distinction drawn in any of those docs, even when the word appears in italics.
I guess one specific question: When the constraints.growthAxisDirection
is right or down, fromScrollOffset
comes directly from a "paint offset" value which doesn't sound like it's about layout. Why is this new adjustment needed in those two cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Yeah, that layoutExtent
doc sure seems to be using "visible" to refer to layout, not paint, doesn't it. :-)
Maybe it'd be more accurate to say that the sliver docs use "visible" ambiguously to mean either layout or paint, and it contrasts only with "outside the viewport". Rereading the childMainAxisPosition
doc, when it says "the leading visible edge" (complete with italics), that may well be the only contrast it means.
I'll expand the comment to say why for this method the intended semantics are about paint, not layout.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess one specific question: When the
constraints.growthAxisDirection
is right or down,fromScrollOffset
comes directly from a "paint offset" value which doesn't sound like it's about layout. Why is this new adjustment needed in those two cases?
Hmm, that does sound suspicious.
… And I think it points to a bug! If I adjust the example app so that the headers go at bottom instead of top (and also set center
to adjust the paint order, so that now the bottom sliver paints last, as it needs to in that case):
--- lib/example/sticky_header.dart
+++ lib/example/sticky_header.dart
@@ -143,5 +143,6 @@ class ExampleVerticalDouble extends StatelessWidget {
semanticChildCount: numSections,
+ center: const ValueKey('center'),
slivers: [
SliverStickyHeaderList(
- headerPlacement: HeaderPlacement.scrollingStart,
+ headerPlacement: HeaderPlacement.scrollingEnd,
delegate: SliverChildBuilderDelegate(
@@ -160,3 +161,4 @@ class ExampleVerticalDouble extends StatelessWidget {
SliverStickyHeaderList(
- headerPlacement: HeaderPlacement.scrollingStart,
+ key: const ValueKey('center'),
+ headerPlacement: HeaderPlacement.scrollingEnd,
delegate: SliverChildBuilderDelegate(
then the painting and hit-testing don't work out correctly if I scroll to a point where the sticky header (now at the bottom) overflows the bottom sliver onto the top sliver.
I'll see about fixing that. Thanks for the careful reading.
d24f01c
to
b398a77
Compare
OK, pushed a new revision. In order to have a better shot at getting it right this time, I added another example, and a new swath of systematic test cases (another layer in that nested loop in Selected commit messages, of new or revised commits: 1a1c501 sticky_header test [nfc]: Make "first/last item" finders more robustThe existing As is, that works because the For examples of using the APIs these finders use, compare the 1907fba sticky_header test: Test slivers splitting viewportThere are still some bugs affecting the sticky_header library when a I sent a PR which aimed to fix a cluster of those, in which I tried So that seems like a sign that this really should get systematic Some of these new test cases don't yet work properly, because they The "header overflowing sliver" skip condition will be removed later The "paint order" skips will be addressed in an upcoming PR. 2b75ad8 sticky_header [nfc]: Fix childMainAxisPosition to properly use paintExtentThis fixes a latent bug: this method would give wrong answers if the The bug is latent because performLayout currently always produces a 5e774ec sticky_header [nfc]: Split header-overflows-sliver condition explicitlyThis makes for fewer situations to think about at a given point in 229434a sticky_header [nfc]: Expand on the header-overflows-sliver caseThis case has several bugs in it. Not coincidentally, it's tricky to One step in sorting that out was the preceding commit which split 7b9ff0d sticky_header: Fix hit-testing when header overflows sliverWhen the sticky header overflows the sliver that provides it -- that But it didn't work properly for hit-testing: trying to tap the Specifically, this sliver was reporting to its parent that its Fix by accurately reporting this sliver's paint region (via This fix lets us mark a swath of test cases as no longer skipped. On the other hand, this change introduces a different bug in this Other than that, after this fix, sticky headers overflowing into |
Great! Thanks; I appreciate your careful response to the point I raised in my last review. Thanks also for answering my questions in the office. LGTM, please merge at will. |
This is the class we actually use at this point -- not StickyHeaderListView -- so it's good for it to have some docs too.
This way the example can be used to demonstrate the next cluster of bug fixes working correctly, before yet fixing the next issue after those.
This is nearly NFC. The sense in which it isn't is that if any of the `tester.drag` steps touched a header -- which listens for tap gestures -- then this would ensure that step got interpreted as a drag. The old version would (a) interpret it as a tap, not a drag, if it was less than 20px in length; (b) leave those first 20px out of the effective length of the drag. The use of DragStartBehavior.down fixes (b). Then there are some drags of 5px in these tests, subject to (a); TouchSlop fixes those (by reducing the touch slop to 1px, instead of 20px). This change will be needed in order to have the item widgets start recording taps too, like the header widgets do, without messing up the drags in these tests.
This will be useful in a test we'll add for an upcoming change.
These changes are NFC for the existing double-sliver example, with the sticky header at the top.
The existing `.first` and `.last` versions rely on the order that children appear in the Flutter element tree. As is, that works because the `_Item` widgets are all children of one list sliver, and it manages its children in a nice straightforward order. But when we add multiple list slivers, the order will depend also on the order those appear as children of the viewport; and in particular the items at the edges of the viewport will no longer always be the first or last items in the tree. So introduce a couple of custom finders to keep finding the first or last items in the sense we mean. For examples of using the APIs these finders use, compare the implementation of [FinderBase.first].
This will be helpful for keeping complexity down when we add more slivers to this list.
This is still enough to fill more than the viewport, and will be more helpful for scrolling past an entire sliver when we add more slivers.
There are still some bugs affecting the sticky_header library when a SliverStickyHeaderList occupies only part of the viewport (which is a configuration we'll need for letting the message list grow in both directions, for zulip#82). I sent a PR which aimed to fix a cluster of those, in which I tried to get away without writing these systematic test cases for them. It worked for the cases I did test -- including the cases that would actually arise for the Zulip message list -- and I believed the changes were correct when I sent the PR. But that version was still conceptually confused, as evidenced by the fact that it turned out to break other cases: zulip#1316 (comment) So that seems like a sign that this really should get systematic all-cases tests. Some of these new test cases don't yet work properly, because they exercise the aforementioned bugs. The "header overflowing sliver" skip condition will be removed later in this series. The "paint order" skips will be addressed in an upcoming PR.
…xtent This fixes a latent bug: this method would give wrong answers if the sliver's paintExtent differed from its layoutExtent. The bug is latent because performLayout currently always produces a layoutExtent equal to paintExtent. But we'll start making them differ soon, as part of making hit-testing work correctly when a sticky header is painted by one sliver but needs to encroach on the layout area of another sliver. The framework calls this method as part of hit-testing, so that requires fixing this bug too.
This makes for fewer situations to think about at a given point in the code, and will make the logic a bit easier to follow when we make some corrections to the overflow case.
This commit is NFC for the actual app, or at least nearly so. This call to calculatePaintOffset was conceptually wrong: it's asking how much of this sliver's region to be painted is within the range of scroll offsets from zero to headerExtent. That'd be a pertinent question if we were locating something in that range of scroll offsets... but that range is not at all where the header goes, unless by happenstance. So the value returned is meaningless. One reason this buggy line has survived is that the bug is largely latent -- we can remove it entirely, as in this commit, and get exactly the same behavior except in odd circumstances. Specifically: * This paintedHeaderSize variable can only have any effect by being greater than childExtent. * In this case childExtent is smaller than headerExtent, too. * The main way that childExtent can be so small is if remainingPaintExtent, which constrains it, is equally small. * But calculatePaintOffset constrains its result, aka paintedHeaderSize, to at most remainingPaintExtent too, so then paintedHeaderSize still won't exceed childExtent. I say "main way" because the alternative is for the child to run out of content before finding as much as headerExtent of content to show. That could happen if the list just has less than that much content; but that means the header's own item is smaller than the header, which is a case that sticky_header doesn't really support well anyway and we don't have in the app. Otherwise, this would have to mean that some of the content was scrolled out of the viewport and then the child ran out of content before filling its allotted remainingPaintExtent of the viewport (and indeed before even reaching a headerExtent amount of content). This is actually not quite impossible, if the scrollable permits overscroll... but making it happen would require piling edge case upon edge case. Anyway, this call never made sense, so remove it. The resulting code in this headerExtent > childExtent case still isn't right. Removing this wrong logic helps clear the ground for fixing that.
This case has several bugs in it. Not coincidentally, it's tricky to think through: there are several sub-cases and variables involved (the growth direction, vs. the header-placement direction, vs. the coordinate direction, ...). And in fact my original PR revision which fixed the cases that would affect the Zulip message list was still conceptually confused, as evidenced by the fact that it turned out to break other cases: zulip#1316 (comment) One step in sorting that out was the preceding commit which split this overflows-sliver case from the alternative. As a next step, let's expand on the reasoning here a bit, with named variables and comments. In doing so, it becomes more apparent that several points in this calculation are wrong; for this NFC commit, mark those with TODO-comments. We'll fix them shortly.
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. Specifically, this sliver was reporting to its parent that its paint region (described by `paintOrigin` and `paintExtent`) was the same as the child sliver's paint region. In reality, this sliver in this case will paint a larger region than that. Fix by accurately reporting this sliver's paint region (via `paintOrigin` and `paintExtent`), while adjusting `headerOffset` to be relative to the new truthful paint region rather than the old inaccurate one. This fix lets us mark a swath of test cases as no longer skipped. On the other hand, this change introduces a different bug in this overflow case: the child sliver is now painted in the wrong place if _headerAtCoordinateEnd(). (It gets lined up with the inner edge of the header, even though it's too short to reach the viewport edge from there.) That bug is latent as far as the Zulip app is concerned, so we leave it for now with a TODO. Other than that, 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 in an upcoming PR.
b398a77
to
043ae10
Compare
Thanks! Merged. |
This is the next round after #1312 (and is stacked atop #1312), fixing another cluster of latent bugs in the sticky_header library to prepare for having two slivers share space back to back in the list.
After #1312, the headers paint correctly in that case. But when the sliver boundary is scrolled to within the header, the hit-testing behavior isn't yet right: trying to tap on the bottom part of the header, where it's overflowing over the bottom sliver, ends up hitting the bottom sliver instead of the header. This PR fixes that.
… Well, it does if the viewport gives the slivers the right paint order (and hit-test order). Making that happen with back-to-back slivers, like the message list needs, requires some more code; and this PR is long enough already. So we'll save that for the next PR.
Selected commit messages
54c2b68 sticky_header example: Enable ink splashes, to demo hit-testing
2c3a6ea sticky_header example: Set allowOverflow true in double-sliver example
421415b sticky_header [nfc]: Fix childMainAxisPosition to handle paintOrigin nonzero
This fixes a latent bug: this method would give wrong answers if the
sliver's paintOrigin were nonzero. See the new comments.
The bug is latent because performLayout currently always produces a
zero paintOrigin. But we'll start using paintOrigin soon, as part of
making hit-testing work correctly when a sticky header is painted by
one sliver but needs to encroach on the layout area of another sliver.
The framework calls this method as part of hit-testing, so that
requires fixing this bug too.
a76ba67 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 wouldactually paint in the
paint
method. When hit-testing, thatreported geometry gets interpreted by the framework code before
calling this render object's other methods.
Fix that by reporting an accurate
paintOrigin
andpaintExtent
.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.
90e4b5f sticky_header [nfc]: Doc overflow behavior and paint-order constraints