Skip to content
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

Add ability to control scroll animation duration for Android #17422

Closed
wants to merge 15 commits into from

Conversation

dannycochran
Copy link
Contributor

@dannycochran dannycochran commented Jan 3, 2018

Motivation

This is one of the more sought after feature requests for RN:
https://react-native.canny.io/feature-requests/p/add-speed-attribute-to-scrollto

This PR adds the support to add a "duration" whenever using "scrollTo" or "scrollToEnd" with
a scrollView. Currently this only exists for Android as the iOS implementation will be somewhat more involved.

This PR is also backwards compatible and does not yet deprecate the "animated" boolean. It may not make sense to ever deprecate "animated", as it could be the flag that is used when devs want the system default duration (which is 250ms for Android). I'm not sure what it is for iOS. It would simplify things to remove "animated", though.

Test Plan

  1. Create a ScollView component that overflows the bounds of the container, e.g.:
  render() {
    return (
      <View style={styles.container}>
        <Button onPress={this.scrollToPosition} title="Scroll to Position" color="#841584" />
        <ScrollView style={styles.scrollView} ref={scrollView => this.scrollView = scrollView}>
          {items.map(item => {
            return <View key={item.key} style={styles.scrollViewItem}><Text>{item.text}</Text></View>
          })}
        </ScrollView>
      </View>
    );
  }
  1. Using the "scrollView" ref and the Button callback, scroll to a position with a desired duration speed, e.g:
scrollToPosition = () => {
  this.scrollView && this.scrollView.scrollTo({
    y: 500
    duration: 1000
  });
};
  1. Ensure the legacy "animated" boolean works as expected:
scrollToPosition = () => {
  this.scrollView && this.scrollView.scrollTo({
    y: 500
    animated: true
  });
};
  1. Test that you can animate the x position as well (change the ScrollView to be horizontal).
  2. Test that the scroll animation is canceled when touching on the scroll view in the middle of an animation.
  3. Test that repeated calls to animate the scrollView don't queue up (e.g. smashing the in the example above, and then tapping into the scrollView should cancel any animation movement).

Vertical scroll:
https://drive.google.com/file/d/1VhaolHx63GOv0YWTg7ay0GtzW1nIk635/view?usp=sharing

Horizontal scroll:
https://drive.google.com/file/d/1yq5P1k4-KPPu9BhS6B9liL-NRGX1Z4gA/view?usp=sharing

Related PRs

Abandoned PR to bring "duration" to iOS:
#1334

Updated docs on react-native-website:
facebook/react-native-website#115

Release Notes

[ANDROID] [ENHANCEMENT] [ScrollView] - Added support for controlling animation duration speed for methods "scrollTo" and "scrollToEnd"

@facebook-github-bot
Copy link
Contributor

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need the corporate CLA signed.

If you have received this in error or have any questions, please contact us at [email protected]. Thanks!

@facebook-github-bot
Copy link
Contributor

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jan 3, 2018
@facebook-github-bot
Copy link
Contributor

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@janicduplessis
Copy link
Contributor

Thanks for working on this!

Got a few questions just to make sure, does the technique you used to animate the scroll position is exactly the same as ScrollView#smoothScrollTo (same animation easing curve, etc) and have you tested cancelling animations (either tapping or starting a second animation while the first is still running)?

dannycochran added a commit to dannycochran/react-native-website that referenced this pull request Jan 3, 2018
facebook/react-native#17422 adds the ability to control the duration of a scrollView animation, for Android only.
@dannycochran
Copy link
Contributor Author

dannycochran commented Jan 3, 2018

@janicduplessis thanks for the response. I imagine we'll have to go through a few iterations here as this is my first PR with react-native :-)

  1. The duration should be the same as "smoothScrollTo" (which calls startScroll, which defaults to a 250ms duration), but I'm not sure the easing curve is. If we wanted to, I could use the existing "smoothScrollTo" for when users specify {animated: true}, but if we just look at duration things get a little simpler from a code complexity perspective.

  2. I need to test this some more. What should the desired behavior be? If a user drags the scrollView in the middle of an animation, should it cancel the existing animation? That probably is the best behavior, and if devs want to prevent an animation from being canceled they set "pointerEvents='none'".

@dannycochran
Copy link
Contributor Author

@janicduplessis I fixed it so that repeated calls to animate the ScrollView would cancel the previous animation, as well as canceling an animation that was interrupted by a touch event.

As for the interpolation, it looks like "smoothScrollTo" used ViscousFluidInterpolator, which is not public:
https://android.googlesource.com/platform/frameworks/base.git/+/master/core/java/android/widget/Scroller.java#561

Whereas ObjectAnimator defaults to the AccelerateDecelerateInterpolator:
https://developer.android.com/reference/android/view/animation/AccelerateDecelerateInterpolator.html

We could either just go with AccelerateDecelerateInterpolator, or copy in the ViscousFluidInterpolator code. I'm fine with either approach. The existing "smoothScrollTo" animation duration is so fast it's pretty difficult to notice the difference between interpolation methods.

@dannycochran
Copy link
Contributor Author

@janicduplessis Anything else needed on my end? The failing Android test appears to be unrelated.

@alpamys-qanybet
Copy link

@dannycochran, Hi there,
I have implemented your code for Android, everything works ok, built from source, now I need to implement it in ios.
pull request for ios seems to be on forked project, not react-native itself.
#1334
how can I implement it for ios?

@alpamys-qanybet
Copy link

@dannycochran,
I implemented the code for iOS, however I do not know iOS with this commit, also, there is your code too.
Can you or somebody else who know iOS check the code?

Especially this part

     [UIView animateWithDuration:duration
          delay:0
        options:UIViewAnimationOptionBeginFromCurrentState
     animations:^(void) {
       [_scrollView setContentOffset:offset];
     } completion:^(__unused BOOL finished) {
     }];

@sahrens
Copy link
Contributor

sahrens commented Jan 19, 2018

cc @shergin for iOS code.

@sahrens
Copy link
Contributor

sahrens commented Jan 19, 2018

cc @skevy as well since he wrote the original implementation.

@dannycochran
Copy link
Contributor Author

Pinging anyone who might be available to review :-) @janicduplessis @sahrens @skevy @shergin

* Helper method for animating to a ScrollView position with a given duration,
* instead of using "smoothScrollTo", which does not expose a duration argument.
*/
public static ObjectAnimator animateScroll(final ViewGroup scrollView, int mDestX, int mDestY, int mDuration) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea if this Animator object is legit - @janicduplessis or @mdvacca?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has support from API 11:

https://developer.android.com/reference/android/animation/ObjectAnimator.html

FWIW we've been using this ScrollView (via overwriting the ScrollView package in MainApplication.Java) in production for the past 6 weeks, in production / release. We also use it in a fairly expensive SectionList which does all sorts of crazy optimizations and is a good stress test for performance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still like signoff from someone that knows our android codebase...@janicduplessis, @mdvacca, or @axe-fb?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

}
this.getScrollResponder().scrollResponderScrollTo(
{x: x || 0, y: y || 0, animated: animated !== false}
{x: x || 0, y: y || 0, animated: animated !== false, duration: duration || 0}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duration || 0 seems wrong - should probably just send the null. I could imagine people explicitly setting 0 as an equivalent to animated: false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the existing pattern with this API where a default value is always provided.

In ReactScrollViewCommandHelper.java, a duration of zero is effectively ignored.

It's tricky, though, because we want to support the existing animated API so this is backwards compatible. So what happens if a user says "animated: true, duration: 0", or "animated: false, duration: 250"? In ReactScrollViewCommandHelper, I defer first to duration value. If duration is non-zero, we animate with that duration. Otherwise, if animated is true, we use the legacy animation duration (250).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, thought I replied earlier.

I'm not too concerned about cases where users give ambiguous input. If a user says "animated: true, duration: 0" there is no right answer, so we can do whatever - undefined behavior.

If the user calls scrollTo({y, duration: 0}) it's very clear that they don't want it to animate, and we should respect that clear intention and not animate it rather than making them figure out they should set animated: false instead. At the very least we could add an invariant that catches that case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, I was missing the part where animated defaults to true in this code, and we'd probably want to preserve that, but then not respect it if a user passes duration: 0.

@dannycochran
Copy link
Contributor Author

@sahrens any update here? I replied to your comments above. I'm happy to make some changes if you'd like but it'd be nice if we could have a more regular back and forth cadence :)

Copy link
Contributor

@sahrens sahrens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, let's just pass null instead of 0 by default and ignore it so that 0 is a valid input that effectively disables animation.

*
* Example:
*
* `scrollTo({x: 0, y: 0, animated: true})`
* `scrollTo({x: 0, y: 0, animated: true, duration: 0})`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a good example because it's unclear what this should do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a separate example just for duration.

@dannycochran
Copy link
Contributor Author

@mdvacca you know it made a lot more sense to just put the "mAnimator" object inside the ScrollView itself, and have the managers call "animateScroll". That way the ScrollView can use its "onTouchEvent" to cancel the mAnimator if there's one currently in flight:

6161dd8

Tested locally and it's working on both vertical / horizontal scroll views.

@dannycochran
Copy link
Contributor Author

@mdvacca @sahrens Anything else needed here? Would love to get this merged :-)

@dannycochran
Copy link
Contributor Author

@grabbou pinging for help here on a reviewer as well. I seem to have gotten sign off on the JS and Android side of things, but still need an official approval.

@grabbou
Copy link
Contributor

grabbou commented Apr 10, 2018

Would be great to have a word from @janicduplessis before landing as he did some comments previously.

CC: @hramos this PR has been already approved by few developers and is looking for official (and final) approval to land it. Would you be able to help or find someone who can look into it?

@@ -558,29 +558,36 @@ const ScrollView = createReactClass({
},

/**
* Scrolls to a given x, y offset, either immediately or with a smooth animation.
* Scrolls to a given x, y offset, either immediately, with a smooth animation, or,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open a PR that applies these same changes to https://github.com/facebook/react-native-website/blob/master/docs/scrollview.md ?

These comments are not synced. At some point we need to update these comments with links to the generated docs at http://facebook.github.io/react-native instead (I've done this with a few source files already but had not gotten to ScrollView yet).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hramos
Copy link
Contributor

hramos commented Apr 10, 2018

Looks good to me, but looks like we'd need confirmation from @janicduplessis and others.

Please reach out to them via Slack or other means.

@facebook-github-bot
Copy link
Contributor

@dannycochran 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.

@dannycochran
Copy link
Contributor Author

@facebook-github-bot @janicduplessis I've rebased changes here. This change remains relevant and one of the most upvoted feature requests in Canny.

Copy link
Contributor

@janicduplessis janicduplessis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make this so that it uses smoothScrollTo when using the default duration (just {animated: true})? I'd rather keep the default android behavior and this will avoid changing the scrolling behavior in subtle ways.

Looking at the source for smoothScrollTo it seems to do a lot more than just animating the scroll position. See https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/ScrollView.java#1103 and https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/widget/OverScroller.java. Sadly there isn't a way to customize the OverScroll instance easily (maybe possible with some crazy reflection stuff).

@janicduplessis
Copy link
Contributor

Also cc @mdvacca :)

@facebook-github-bot
Copy link
Contributor

@dannycochran 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.

@sahrens
Copy link
Contributor

sahrens commented May 26, 2018

Thanks for reviewing, @janicduplessis!

@dannycochran: can you make those changes? I agree this would be a nice improvement :)

@evanjmg
Copy link

evanjmg commented Nov 1, 2018

Any update on this? @dannycochran can you fix your conflicts?

@kabus202
Copy link

Any updates on this?

@paulmand3l
Copy link
Contributor

Ping. This is so close, it would be a shame for it to get dropped now.

@osdnk
Copy link
Contributor

osdnk commented Jan 6, 2019

I would love to have it merged, @janicduplessis @mdvacca @dannycochran

@hramos
Copy link
Contributor

hramos commented Jan 12, 2019

In cases like this, where a pull request has been thoroughly reviewed but still remains in a "Changes Requested" state, I'd encourage the community to "commandeer" the PR by creating their own pull request that incorporates the requested changes. If anyone does this, please ping me or Spencer and we'll take a look.

@hramos
Copy link
Contributor

hramos commented Jan 14, 2019

Thanks @osdnk for picking up the work: #22884 is a copy of this PR with resolved conflicts.

@cpojer
Copy link
Contributor

cpojer commented Jan 29, 2019

Closing in favor of #22884.

@cpojer cpojer closed this Jan 29, 2019
cpojer pushed a commit to facebook/react-native-website that referenced this pull request Feb 7, 2019
* Update scrollview.md to reflect duration argument

facebook/react-native#17422 adds the ability to control the duration of a scrollView animation, for Android only.

* sync comments with ScrollView.js
sunnylqm added a commit to reactnativecn/react-native-website that referenced this pull request Feb 8, 2019
* [android] document required Network Security Config for React Native packager (#772)

* add documentation for applying Network Security Config cleartext policy exceptions for RN packager IPs

* add documentation for applying Network Security Config cleartext policy exceptions for RN packager IPs

* Update navigation.md (#770)

Use createAppContainer instead of createAppNavigator 
see function declaration here https://github.com/react-navigation/react-navigation/blob/master/src/react-navigation.js#L5

* Update README.md

Fixed a few spelling errors in README.md

* Deprecating NavigatorIOS (#776)

* Deprecating NavigatorIOS

* Remove navigator ios

* Remove TabBarIOS, TabBarIOS.Item, NavigatorIOS from docs

* Make Hello World prettier (#760)

Fix #743

* Remove outdated Stetho documentation.

* Fix Fresco docs link on the Image Style Props page (#783)

* Remove CLI docs (#782)

* Update scrollview.md to reflect duration argument (#115)

* Update scrollview.md to reflect duration argument

facebook/react-native#17422 adds the ability to control the duration of a scrollView animation, for Android only.

* sync comments with ScrollView.js

* add documentation about `.native.js` extensions (#346)

* add documentation about `.native.js` extensions

Lots of projects use the `.native.js` to share code between ReactJS and React Native (including core projects, like Jest) but there's no official documentation about it. I'm adding some info so projects can easily discover and refer to this feature.

* fix comments and add pro-tip about `.native.js`

Fixed the comment on a code block and added a pro-tip telling users to ignore `.native.js` extensions in their web bundlers in order to optimize final bundle size.

* Update platform-specific-code.md

* Link to all KeyboardType screenshots (#774)

* Node version requirement made more prevalent (#775)

Reading the first page of https://facebook.github.io/react-native/docs/getting-started.html, it is not clear that the node version needs to be 8+. Just adding this here to help others with installation, specially those of us who already have node running and need to upgrade.

* Explicitly state that `activeOpacity` requires `underlayColor` (#781)

* Explicitly state that `activeOpacity` requires `underlayColor`

It's clear from facebook/react-native#11834 that the underlying issue won't be fixed, so we should update the documentation so people understand that for `activeOpacity` to work, they need to set `underlayColor`.

* Update touchablehighlight.md

* Add section : Known Issues with fetch and [..] (#769)

* Add section : Known Issues with fetch and [..]

Cookie based authentication is currently wobbly. Had it not been for the problematic redirect scenario using fetch workarounds could be found (e.g clearing cookies before every single request and manually adding them to the headers). However, because of the 302 issue there is a scenario where using cookies can result in halt of development.

The rest of the issues are merely added here so that people that can follow workarounds ( please take a look as unintended consequences might occur )

* Minor text changes and remove the last paragraph.

* Document how to publish custom React Native version (#675)

* Add section for how to make a git dependency branch (#653)

* move publishing a fork to separate page (#653)

* updated review comments

* link back from building android sources and updated docker link

* changed to —force instead of changing .gitignore

* Update publishing.md

* Update correct signing information for iOS (#766)

Ran into this problem while following this guide verbatim.

* Update Wix link and title in showcast.json (#763)

* change showcast wix logo app

* fix file name

* Update Wix link and title in showcast

* Change wording to improve sentence (#759)

* Text/TextInput: Android now supports `maxFontSizeMultiplier` prop (#755)

Introduced by this commit: facebook/react-native@4936d28

* Fix typo (#738)

* Fix typo

* Update linking-libraries-ios.md

* update to rn-diff-purge (#761)

* Prettify

* Sync guides
@avinashpatnaik
Copy link

avinashpatnaik commented Apr 6, 2020

Was this released? It doesnt seem to work in Android when I get the react-native from 'https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz'. I checked the ReactScrollViewCommandHelper(I am assumimg this is where the changes happened) and there is no duration variable available yet. Am I missing something here?

I am verifying my changes in an Android emulator.

@vadim-tarasenko
Copy link

You can use reanimated to make it work.

const offsetY = useSharedValue(0);

const animatedProps = useAnimatedProps<FlatListProps<unknown>>(() => {
  return {
    contentOffset: {
      x: 0,
      y: offsetY.value,
    },
  };
});

const handleScroll = () => {
  offsetY.value = withTiming(targetIndex * CARD_HEIGHT, {
    duration: YOUR_DURATION_HERE,
  });
}

return <Animated.FlatList animatedProps={animatedProps} ... />

JackWillie added a commit to JackWillie/react-native-website that referenced this pull request Nov 27, 2022
* Update scrollview.md to reflect duration argument

facebook/react-native#17422 adds the ability to control the duration of a scrollView animation, for Android only.

* sync comments with ScrollView.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Platform: Android Android applications. Ran Commands One of our bots successfully processed a command. Type: Enhancement A new feature or enhancement of an existing feature.
Projects
None yet
Development

Successfully merging this pull request may close these issues.