-
Notifications
You must be signed in to change notification settings - Fork 24.4k
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
Fix VirtualizedList with maintainVisibleContentPosition #35993
Fix VirtualizedList with maintainVisibleContentPosition #35993
Conversation
Working on adding some tests |
|
Base commit: cff4bc8 |
Very excited by this. cc @rozele as well who was looking at scroll anchoring. This should solve a huge class of issues with relayout pushing items around during realization. But it also actually would allow us to implement APIs like scrollToEnd() in a way which avoids flutter as well (since as of 0.71+ we can realize the end cell ahead of time and use as an anchor if we scroll down before other content is rendered.). Going to try to dedicate some time to read through all of this. Have a feeling this will be one of those changes that requires a cup of coffee and some quiet time to grep through 😛. |
I've been testing this change for a while and for the most part it works great. However, sometimes when adding a lot of items at once it still seems to lose the anchor. I've recorded a short video to demonstrate the issue. In the video it works great the first three times I reset the app and add items to the list, but then fails and loses anchor after more resets when adding items. Each time I press the red square, Simulator.Screen.Recording.-.iPhone.14.-.2023-02-23.at.12.22.44.mp4 |
@mikfoo Could you provide me with the code you used to reproduce this problem? |
6c56bbd
to
217fecb
Compare
@janicduplessis: Sure. If you create a new RN project and apply the patch manually to With the code in the gist I can reliably reproduce the issue over and over. It usually happens on first "Add" and also when you have scrolled to the top of the list (when Another small thing I noticed (not sure if it's directly related to this PR) is that when you add new items too fast to the list (eg. pressing the "Add" button a couple of times in fast succession) then it throws an error: |
I think this will also work for the inverted case on mobile platforms, since that just uses transform tricks to make the start of the list render at the bottom, but it might be worth validating. For platforms like Web and Windows, we actually need to change the way inversion works because the transform tricks don't play nice with keyboard nav, mouse wheel events, etc. Anecdotally, on Windows, we have a prototype for this (microsoft/react-native-windows#8440). It feels a bit like the hacks to get things to work in VirtualizedList are piling up 😅. I wonder how many of these could be resolved with a native virtualized list component (render mask for retaining focused islands, maintainVisibleContentPosition virtualization, native inversion for relevant platforms)... |
I also wonder if you could build on @NickGerleman's render mask functionality to implement this. E.g., add a new event when a scroll anchor is selected and force an "island" of materialized cells around the selected anchor. Not sure it would be feasible, given the virtualization logic runs from onLayout and onScroll, and you don't select an anchor for maintainVisibleContentPosition until ScrollView layout (which would be too late to prevent the bug). I've also long wondered if there's a better alternative to maintainVisibleContentPosition. Here are some ideas I've had in the past:
If we switched to "anchor-by-default" behavior and assumed that an anchor was selected on every commit, we could be certain an event would fire with the selected anchor in VirtualizedList 🤷♂️. This is all just food for thought. I have no opposition to fixing bugs for code that should already work 😅 |
@janicduplessis I played with it again last night, and noticed that if you add items while it's batch rendering then it always loses focus if you're at the top of the list at least. If you wait adding items to the list until it's completely done rendering, then the focus is kept (except on the initial render, it always loses focus on that). |
1076790
to
3379aec
Compare
@mikfoo Thanks for testing this! I was able to reproduce the issue with the code you provided and it should now be fixed with 3379aec. In the example I used to test I added items during scrolling with I can also reproduce the issue where the first time items are added the adjustment doesn't work. Will have a look at this next. |
@rozele Thanks for having a look. I agree with you that a better long term solution would be great to explore, but in the meantime this is an acceptable fix. I am pretty sure inverted works, but will double check.
Unless I'm wrong about what you mean here, this is already what I do here. Basically what I do is detect that the scrollview will be anchored and create an island around previously visible cells. I think the idea of having anchor events would be great in this case, since currently I have to approximate that a scroll adjustment will happen, since that logic is implemented in native code using layout info not available to JS. |
3379aec
to
f673c7d
Compare
@janicduplessis Awesome! I just tested the changes and they seem to work great! |
I was able to take a look through the code, and description. I have some implementation comments, but before I leave those, I wanted to check to see if I understand the overall problem, and the viability of an alternative method of doing this. From the flow, here, it seems like:
What I am wondering, could we attack #2 by changing how VirtualizedList handles That would avoid needing to keep an arbitrarily large range of cells rendered, because the VirtualizedList decided to move its own point of reference in response to items shifting index. |
A comment pending on this implementation, that I think would effect that one as well, is that we usually want to avoid the full scan of Though I am not sure the current API provides a way to diff before/after efficiently, without searching for keys. But we could also maybe optimize that search (e.g. look around the last index first). |
@NickGerleman Thanks for having a look, if I understand your point correctly you suggest adjusting the
Yea the best I can think of is to use the change in data size and start the lookup from there. So if items are added at the start the old first item will be at index |
My recommendation is that we shift the old window, to a single new location, instead of rendering two windows. Then we explicitly rely on On data change, VirtualizedList's strategy of rendering The existing index-based alignment may produce a sroll position with is off from the previous data, but even off from the new data at the desired index. Since the previous spacer above the content was using most recent cell measurements, which may have changed. So we need If we instead realign
For the reasons above, I think we may want to do this realignment in more general cases. Thinking more about this, it should be possible to do efficiently, and with a bounds in common cases. Just slower compared to us having information ahead of time.
That kinda veers on having |
Though also FYI @rozele @acoates-ms that would add a dependency on maintainVisibleContentPosition, which I think will soon only be a concern for Windows. |
f673c7d
to
45a0225
Compare
@NickGerleman Hopefully I got your feedback right, as I'm not familiar with all of VirtualizedList code. I changed it so that instead of creating a 2nd window with the mvcp adjustment I instead update the
I'm not sure I understand here what you mean by doing a realignment in more general cases, also the part about creating a mapping from old to new index I added a hint to |
The idea is that we adjust |
for (let ii = 0; ii < props.getItemCount(props.data); ii++) { | ||
const curKey = VirtualizedList._getItemKey(props, ii); | ||
if (curKey === key) { | ||
return ii; | ||
} | ||
} | ||
return null; | ||
} |
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 agree with your earlier comment about this being fine for only when maintainVisibleContentPosition
is enabled. If we wanted to generalize, it would be best to bound to the size of the previous window.
if (item == null) { | ||
return null; | ||
} | ||
return VirtualizedList._keyExtractor(item, 0, props); |
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.
return VirtualizedList._keyExtractor(item, 0, props); | |
return VirtualizedList._keyExtractor(item, index, props); |
…ent moves window before list start Summary: facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Differential Revision: D47846165 fbshipit-source-id: 6e88a3cf6999a8194dea5e7eccea3df3826dbe3d
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Differential Revision: D47846165 fbshipit-source-id: 786c98d8ddbf45153e32e829a246d8de6764a693
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 3dc09262380a0e1739f03fc12648278689819c60
…ent moves window before list start (#38655) Summary: Pull Request resolved: #38655 #35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…ent moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…tion` adjustment moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
…tion` adjustment moves window before list start (facebook#38655) Summary: Pull Request resolved: facebook#38655 facebook#35993 added logic in VirtualizedList to support `maintainVisibleContentPosition`. This logic makes sure that a previously visible cell being used as an anchor remains rendered after new content is added. The strategy here is to calculate the difference in previous and new positions of the anchor, and move the render window to its new location during item change. `minIndexForVisible` is used as this anchor. When an item change moves the anchor to a position below `minIndexForVisible`, shifting the render window may result in a window which starts before zero. This fixes up `_constrainToItemCount()` to handle this. Changelog: [General][Fixed] - Fix invariant violation when `maintainVisibleContentPosition` adjustment moves window before list start Reviewed By: yungsters Differential Revision: D47846165 fbshipit-source-id: 8a36f66fdad321acb255745dad85618d28c54dba
Summary
maintainVisibleContentPosition
is broken when using virtualization and the new content pushes visible content outside its "window". This can be reproduced in the example from this diff. When using a large page size it will always push visible content outside of the list "window" which will cause currently visible views to be unmounted so the implementation ofmaintainVisibleContentPosition
can't adjust the content inset since the visible views no longer exist.The first illustration shows the working case, when the new content doesn't push visible content outside the window. The red box represents the window, all views outside the box are not mounted, which means the native implementation of
maintainVisibleContentPosition
has no way to know it exists. In that case the first visible view is #2, after new content is added #2 is still inside the window so there's not problem adjusting content offset to maintain position. As you can see Step 1 and 3 result in the same position for all initial views.The second illustation shows the broken case, when new content is added and pushes the first visible view outside the window. As you can see in step 2 the view #2 is no longer rendered so there's no way to maintain its position.
Illustration 1
Illustration 2
To fix
maintainVisibleContentPosition
when usingVirtualizedList
we need to make sure the visible items stay rendered when new items are added at the start of the list.In order to do that we need to do the following:
Detect new items that will cause content to be adjusted
The goal here is to know that scroll position will be updated natively by the
maintainVisibleContentPosition
implementation. The problem is that the native code uses layout heuristics which are not easily available to JS to do so. In order to approximate the native heuristic we can assume that if new items are added at the start of the list, it will causemaintainVisibleContentPosition
to be triggered. This simplifies JS logic a lot as we don't have to track visible items. In the worst case if for some reason our JS heuristic is wrong, it will cause extra cells to be rendered until the next scroll event, or content position will not be maintained (what happens all the time currently). I think this is a good compromise between complexity and accuracy.We need to find how many items have been added before the first one. To do that we save the key of the first item in state
firstItemKey
. When data changes we can find the index offirstItemKey
in the new data and that will be the amount we need to adjust the window state by.Note that this means that keys need to be stable, and using index won't work.
Add cells to render mask so that previously visible cells stay rendered
Once we have the adjusted number we can save this in a new state value
maintainVisibleContentPositionAdjustment
and add the adjusted cells to the render mask.This state is then cleared when we receive updated scroll metrics, once the native implementation is done adding the new items and adjusting the content offset.
This value is also only set when
maintainVisibleContentPosition
is set so this makes sure this maintains the currently behavior when that prop is not set.Ignore certain updates while scroll metrics are invalid
While the
maintainVisibleContentPositionAdjustment
state is set we know that the current scroll metrics are invalid since they will be updated in the nativeScrollView
implementation. In that case we want to prevent certain code from running.One example is
onStartReached
that will be called incorrectly while we are waiting for updated scroll metrics.Changelog
[General] [Fixed] - Fix VirtualizedList with maintainVisibleContentPosition
Test Plan
Added bidirectional paging to RN tester FlatList example. Note that for this to work RN tester need to be run using old architecture on iOS, to use new architecture it requires #35319
Using debug mode we can see that virtualization is still working properly, and content position is being maintained.
Screen.Recording.2022-04-13.at.21.09.11.mov