iOS ScrollView: Add scrollBy method#15609
Conversation
Currently, React Native allows you to scroll a ScrollView to a particular position using `scrollTo`. If instead you want to scroll by a delta, you could try to do this using `scrollTo` (add the delta to the current scroll position) but this doesn't always work out as intended due to the async nature of React Native (what you think is the current scroll position might be a stale value). This change introduces a `scrollBy` API to solve this problem. `scrollBy` takes a delta to scroll by and native takes care of doing the arithmetic because it knows the latest scroll position.
| }, | ||
|
|
||
| /** | ||
| * A helper function to scroll by a specific offset in the ScrollView. |
There was a problem hiding this comment.
no-trailing-spaces: Trailing spaces not allowed.
| * | ||
| * `scrollBy(options: {deltaX: number = 0; deltaY: number = 0; animated: boolean = true})` | ||
| */ | ||
| scrollBy: function(options: { deltaX?: number, deltaY?: number, animated?: boolean } ) { |
There was a problem hiding this comment.
no-trailing-spaces: Trailing spaces not allowed.
The change enables developers to ignore scroll events that occur between the time they set the scroll position in JavaScript and the time that the native code applies the scroll position to the view. This is important for apps that care about carefully tracking and managing the ScrollView's scroll position. For example, when a virtualized ScrollView sets the scroll position to X, it wants to begin preparing to render the content as though it's scrolled to X. Currently, if it receives a scroll event for Y, it doesn't know whether the ScrollView reached Y before or after X. Consequently, it doesn't know whether it should start rendering content for Y because Y is the latest scroll position or if it should ignore Y and continue rendering content for X. The way this is implemented is each method for changing the scroll position (e.g. scrollTo, scrollToEnd) returns an id. When the native code sets the scroll position on the view, it fires an `onScrollPositionSet` which contains the id of the related scroll request. Apps can solve the problem described above by ignoring all scroll events that occur between the time that the app requests the scroll position change and the time the corresponding `onScrollPositionSet` event fires. **Test Plan** Built a test app that uses the above technique to ignore scroll events that occur between the scroll request and the firing of the corresponding `onScrollPositionSet` event. Logged whether each `onScroll` event should be ignored and verified that the log matched my expectations. Tested this with `scrollTo`, `scrollToEnd`, and `scrollBy`. **Dependency Notes** I built this change on top of my other PR (facebook#15609). If I didn't do this, I'd have to resolve merge conflicts after facebook#15609 was merged. Adam Comella Microsoft Corp.
|
@rigdern Thank you for the is great PRs! Honestly, I am a bit conserned about should we have this or not; I see a great advantage in having the smallest possible set of APIs. Because of asynchronous nature of RN, this method is already asynchronous. So, if we simply implement reliable getter for |
|
@shergin As you point out, you can implement Before implementing |
|
@rigdern Yes, implementing this natively makes this faster but it does not makes it synchronous. It helps a bit and probably fixes your case, but conceptually it is pretty same as my proposed alternative solution. |
|
@shergin I'm not sure I understand your proposal. Can you elaborate? |
|
@rigdern |
|
@shergin let's say you want to scroll by 50 pixels. What happens if by the time your scrollTo is processed, the scroll position has changed such that your currentContentOffset value is off by 10 pixels? Then your scrollTo request will only move you by 40 pixels instead of the 50 that you wanted. Isn't this a problem that your proposal has but my proposal does not? |
|
@rigdern I believe, your proposal has (almost) same issue, unfortunately. Even if the shift itself will be performed at exact amount of pixels, because of async nature of RN, your app will still have no idea what's going on with scrollview. So, your contr-case looks pretty limited to me. |
|
@shergin regarding the app not knowing what the ScrollView is doing, there's another PR I'll be sharing which causes the ScrollView to fire an event after it applies the scrollTo/scrollBy operation. The combination of the event and the scrollBy method have given us a good experience for manipulating the ScrollView. Does this address your concern about this proposal having almost the same problem as yours? |
|
@rigdern Sure, firing |
|
@shergin To be clear, we're not reusing the Can you elaborate on your concern with this solution? |
|
@sahrens Spencer, we need you second opinion (or blessing) here! |
|
Let me guess - are you using this for an inverted scroll list to do an animated reveal of new items at the bottom? If so, have you tried layout animation and do things work well when the scroll position is in the middle of the content somewhere? If not, giving us more context on your scenario would help. I agree with @shergin in general that minimizing the API is generally good, but I can see some cases where scrollBy might be really helpful. |
|
We have a vertical virtualized list view where all of the items are absolutely positioned. The item positions are controlled by A scenario where We initially tried to do this with I added this example to the PR description. |
|
Ah yes, I have proposed a different solution to this problem - I would posit that the current behavior (content jumping when offscreen layout changes) is rarely what should happen and we should change the bahevior of ScrollView to prevent this, so you can append to the top or bottom with no movement of content in the view port. We'll probably need to hate the behavior somehow so we don't break legacy cases, but I think we should change the behavior long term. @shergin: I think you might have a task for this. Thoughts? |
|
@sahrens Can you elaborate on the details of how this To be clear, we are using a custom virtualized list view implementation. We aren't using React Native's |
|
Speaking about current
So, I believe we can solve both problems having trivial Speaking about "content-offset aware relayout": Yes, we discussed it (just created t21772624), and that is great idea, but I am not sure that I know how to implement it. Yet. And looks like we have to have a way to specify exact node which have to define "stable position". |
|
Ha yeah meant "gate" the behavior. Basically, by default, if you add items off the top of the screen, then we should automatically do the compensation you're trying to do manually. |
|
But at the very least we could make it a new prop, like maintainVisibleContentPosition or something. |
|
@sahrens supporting a However, I suspect implementing this logic in @shergin you mentioned these criticisms of
I agree with the desire to keep the surface area small by only adding APIs that are necessary. I'm not sure why you characterize the native implementation as "complex". It looks like a small amount of straightforward code. You mentioned "I am business logic; I have no idea of current/actual ScrollView state;" I wouldn't say it "has no idea". It knows what the
Feel free to do that. We experimented with various solutions on Android and ended up with a native What are the next steps for working towards a conclusion on this PR? |
|
I think scrollBy is pretty simple so pretty low risk and low cost in terms of API bloat, so if we're not sure how long it would take to get a proper solution in place, I think it would be ok to do this in the mean time. Maybe we shouldn't document it to discourage usage though? Are you ok with moving forward on this, @shergin? |
|
Also, are you really not seeing any flicker with this? Seems like you would run the risk of flickering depending on if the layout takes more than a frame to compute or not. Or are you doing something else to try and synchronize, like waiting for onLayout or something? |
I asked my team about this and you are correct. It turns out we ended up seeing flickering on Android. To fix this, we wrote a custom native module which repositions the items and updates the scroll position in the same frame. (It's not that general of a solution. The method takes a list of views, a list of coordinates to move the views to, and a delta to scroll by and then applies all of that information in the same frame.) So we're no longer using this On iOS, we're still using this |
|
Oh another issue - does scrollBy (or your custom native module on Android) maintain scroll momentum nicely? Here's another approach - what about rendering an "infinite" container on top of your content with a big |
|
Regarding scroll momentum, we designed our virtualized list view to avoid interfering with scroll momentum. It does this by waiting for the scroll position to stop changing (so the ScrollView is probably idle) before setting the scroll position. I know that By the way, I think it's unlikely that my team will be able to switch to a generalized virtualized list. We wrote 2 custom virtualized lists. The first one was intended to handle all virtualized list scenarios in our app (we open sourced it: https://microsoft.github.io/reactxp/docs/extensions/virtuallistview.html). However, one of the virtualized list scenarios in our app was more complicated than the others so we ended up writing a second customized virtualized list just for that scenario. This second more complicated one is the one that uses |
|
... Flickering on Android supports my theory (where the app does not really know what's going on and ask ScrollView to scroll by some amount of pixels). So, we are lucky, this solution works on iOS for this very specific case, but it does not work on Android because... we are not lucky on Android. This make me feel uncomfortable with this approach. |
|
If it waits for scrolling to stop to adjust scroll position, it must also wait to insert the new item, right? Otherwise it would jump. In that case, what happens if you scroll continuously until the ScrollView stops at the edge - would the new content pop in at that point? Looks like your VirtualListView is very similar to VirtualizedList. That's cool you keep the DOM order constant and change the offsets. I thought about doing that, but it made it hard to leverage the power of native layout and LayoutAnimation for cell height changes and animated drag-and-drop. What does recycling actually do? Looks like all it does is recycle the info objects which are very lightweight? There is quite a bit of code though so I probably missed something. I found recycling actually had little benefit and in some cases was actually worse because the react diffing of totally different items was more expensive than just rendering new, and the bottleneck is the js, not the native view creation. The JS VM GC is also very good at short lifecycle object management, unlike say dalvik, so I saw little benefit from recycling js objects. In any case, have you tried a side-by-side perf test with VirtualizedList? I'm curious how they compare. |
I'm not sure what you mean by this. If you mean that the flickering happens because the |
|
@rigdern I tried to find reviewers for this pull request and wanted to ping them to take another look. However, based on the blame information for the files in this pull request I couldn't find any reviewers. This sometimes happens when the files in the pull request are new or don't exist on master anymore. Is this pull request still relevant? If yes could you please rebase? In case you know who has context on this code feel free to mention them in a comment (one person is fine). Thanks for reading and hope you will continue contributing to the project. |
|
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Maybe the issue has been fixed in a recent release, or perhaps it is not affecting a lot of people. If you think this issue should definitely remain open, please let us know why. Thank you for your contributions. |
|
Thanks for the discussion. We decided to go with an alternative solution. We built a native module which combines a number of operations our virtual list view implementation requires (e.g. scrolling & repositioning items) into a single method so all of the operations are completed in the same frame. |
|
|
Android PR: #15610
General Motivation
Currently, React Native allows you to scroll a ScrollView to a particular position using
scrollTo. If instead you want to scroll by a delta, you could try to do this usingscrollTo(add the delta to the current scroll position) but this doesn't always work out as intended due to the async nature of React Native (what you think is the current scroll position might be a stale value). This change introduces ascrollByAPI to solve this problem.scrollBytakes a delta to scroll by and native takes care of doing the arithmetic because it knows the latest scroll position.Example
We have a vertical virtualized list view where all of the items are absolutely positioned. The item positions are controlled by
AnimatedValuesbecause we want to avoid having to go through the diffing process to move them.A scenario where
scrollByis useful is when we load items above the user's viewport which causes theyposition of items in the viewport to change. If we didn't do anything additional, this would cause the items the user is looking at to fly off of the screen. To avoid this, we update the scroll position (by the amount theycoordinate of the viewport items changed) and update theycoordinates at the same time.We initially tried to do this with
scrollToby adding the current scroll position to theycoordinate delta. However, sometimes the current scroll position value was stale by the timescrollToran which caused us to scroll to the wrong position and for the user to perceive a jumpiness. Consequently, we've introduced a nativescrollByimplementation so we don't have to worry about using a stale value for current scroll position.Test Plan
In a test app, verified that
scrollByworks properly with and without animation and for both horizontal and verticalScrollViews. Also, my team is using this change in our app.Adam Comella
Microsoft Corp.