Add ability to control scroll animation duration for Android (#22884)

Summary:
Motivation:
----------
This is one of the more sought after feature requests for RN:
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.
Pull Request resolved: https://github.com/facebook/react-native/pull/22884

Differential Revision: D13860038

Pulled By: cpojer

fbshipit-source-id: f06751d063a33d7046241c95348b6abbb327d36f
This commit is contained in:
Michał Osadnik
2019-01-29 07:18:09 -08:00
committed by Facebook Github Bot
parent 8afa0378cd
commit 7e8b810499
9 changed files with 131 additions and 26 deletions
@@ -7,6 +7,8 @@
package com.facebook.react.views.scroll;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
@@ -55,6 +57,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
private final Rect mRect = new Rect();
private boolean mActivelyScrolling;
private @Nullable ObjectAnimator mAnimator = null;
private @Nullable Rect mClippingRect;
private @Nullable String mOverflow = ViewProps.HIDDEN;
private boolean mDragging;
@@ -183,6 +186,20 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
awakenScrollBars();
}
/**
* Method for animating to a ScrollView position with a given duration,
* instead of using "smoothScrollTo", which does not expose a duration argument.
*/
public void animateScroll(int mDestX, int mDestY, int mDuration) {
if (mAnimator != null) {
mAnimator.cancel();
}
PropertyValuesHolder scrollX = PropertyValuesHolder.ofInt("scrollX", mDestX);
PropertyValuesHolder scrollY = PropertyValuesHolder.ofInt("scrollY", mDestY);
mAnimator = ObjectAnimator.ofPropertyValuesHolder(this, scrollX, scrollY);
mAnimator.setDuration(mDuration).start();
}
public void setOverflow(String overflow) {
mOverflow = overflow;
invalidate();
@@ -266,6 +283,11 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
return false;
}
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
mVelocityHelper.calculateVelocity(ev);
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_UP && mDragging) {
@@ -171,8 +171,13 @@ public class ReactHorizontalScrollViewManager
@Override
public void scrollTo(
ReactHorizontalScrollView scrollView, ReactScrollViewCommandHelper.ScrollToCommandData data) {
if (data.mAnimated) {
scrollView.smoothScrollTo(data.mDestX, data.mDestY);
if (data.mAnimated && data.mDuration != 0) {
if (data.mDuration > 0) {
// data.mDuration set to -1 to fallbacks to default platform behavior
scrollView.animateScroll(data.mDestX, data.mDestY, data.mDuration);
} else {
scrollView.smoothScrollTo(data.mDestX, data.mDestY);
}
} else {
scrollView.scrollTo(data.mDestX, data.mDestY);
}
@@ -185,8 +190,13 @@ public class ReactHorizontalScrollViewManager
// ScrollView always has one child - the scrollable area
int right =
scrollView.getChildAt(0).getWidth() + scrollView.getPaddingRight();
if (data.mAnimated) {
scrollView.smoothScrollTo(right, scrollView.getScrollY());
if (data.mAnimated && data.mDuration != 0) {
if (data.mDuration > 0) {
// data.mDuration set to -1 to fallbacks to default platform behavior
scrollView.animateScroll(right, scrollView.getScrollY(), data.mDuration);
} else {
scrollView.smoothScrollTo(right, scrollView.getScrollY());
}
} else {
scrollView.scrollTo(right, scrollView.getScrollY());
}
@@ -7,6 +7,8 @@
package com.facebook.react.views.scroll;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.TargetApi;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -53,6 +55,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
private final VelocityHelper mVelocityHelper = new VelocityHelper();
private final Rect mRect = new Rect(); // for reuse to avoid allocation
private @Nullable ObjectAnimator mAnimator = null;
private boolean mActivelyScrolling;
private @Nullable Rect mClippingRect;
private @Nullable String mOverflow = ViewProps.HIDDEN;
@@ -171,6 +174,20 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
awakenScrollBars();
}
/**
* Method for animating to a ScrollView position with a given duration,
* instead of using "smoothScrollTo", which does not expose a duration argument.
*/
public void animateScroll(int mDestX, int mDestY, int mDuration) {
if (mAnimator != null) {
mAnimator.cancel();
}
PropertyValuesHolder scrollX = PropertyValuesHolder.ofInt("scrollX", mDestX);
PropertyValuesHolder scrollY = PropertyValuesHolder.ofInt("scrollY", mDestY);
mAnimator = ObjectAnimator.ofPropertyValuesHolder(this, scrollX, scrollY);
mAnimator.setDuration(mDuration).start();
}
public void setOverflow(String overflow) {
mOverflow = overflow;
invalidate();
@@ -255,6 +272,11 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
return false;
}
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
mVelocityHelper.calculateVelocity(ev);
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_UP && mDragging) {
@@ -32,22 +32,25 @@ public class ReactScrollViewCommandHelper {
public static class ScrollToCommandData {
public final int mDestX, mDestY;
public final int mDestX, mDestY, mDuration;
public final boolean mAnimated;
ScrollToCommandData(int destX, int destY, boolean animated) {
ScrollToCommandData(int destX, int destY, boolean animated, int duration) {
mDestX = destX;
mDestY = destY;
mAnimated = animated;
mDuration = duration;
}
}
public static class ScrollToEndCommandData {
public final int mDuration;
public final boolean mAnimated;
ScrollToEndCommandData(boolean animated) {
ScrollToEndCommandData(boolean animated, int duration) {
mAnimated = animated;
mDuration = duration;
}
}
@@ -74,12 +77,14 @@ public class ReactScrollViewCommandHelper {
int destX = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(0)));
int destY = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(1)));
boolean animated = args.getBoolean(2);
viewManager.scrollTo(scrollView, new ScrollToCommandData(destX, destY, animated));
int duration = (int) Math.round(args.getDouble(3));
viewManager.scrollTo(scrollView, new ScrollToCommandData(destX, destY, animated, duration));
return;
}
case COMMAND_SCROLL_TO_END: {
boolean animated = args.getBoolean(0);
viewManager.scrollToEnd(scrollView, new ScrollToEndCommandData(animated));
int duration = (int) Math.round(args.getDouble(1));
viewManager.scrollToEnd(scrollView, new ScrollToEndCommandData(animated, duration));
return;
}
case COMMAND_FLASH_SCROLL_INDICATORS:
@@ -7,6 +7,8 @@
package com.facebook.react.views.scroll;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
@@ -191,8 +191,13 @@ public class ReactScrollViewManager
@Override
public void scrollTo(
ReactScrollView scrollView, ReactScrollViewCommandHelper.ScrollToCommandData data) {
if (data.mAnimated) {
scrollView.smoothScrollTo(data.mDestX, data.mDestY);
if (data.mAnimated && data.mDuration != 0) {
if (data.mDuration > 0) {
// data.mDuration set to -1 to fallbacks to default platform behavior
scrollView.animateScroll(data.mDestX, data.mDestY, data.mDuration);
} else {
scrollView.smoothScrollTo(data.mDestX, data.mDestY);
}
} else {
scrollView.scrollTo(data.mDestX, data.mDestY);
}
@@ -257,8 +262,13 @@ public class ReactScrollViewManager
// ScrollView always has one child - the scrollable area
int bottom =
scrollView.getChildAt(0).getHeight() + scrollView.getPaddingBottom();
if (data.mAnimated) {
scrollView.smoothScrollTo(scrollView.getScrollX(), bottom);
if (data.mAnimated && data.mDuration != 0) {
if (data.mDuration > 0) {
// data.mDuration set to -1 to fallbacks to default platform behavior
scrollView.animateScroll(scrollView.getScrollX(), bottom, data.mDuration);
} else {
scrollView.smoothScrollTo(scrollView.getScrollX(), bottom);
}
} else {
scrollView.scrollTo(scrollView.getScrollX(), bottom);
}