mirror of
https://github.com/status-im/react-native.git
synced 2025-01-12 10:34:57 +00:00
ScrollView snapToOffsets
Summary: * Added snapToOffsets prop to ScrollView. Allows snapping at arbitrary points. * Fixed pagingEnabled not being overridden by snapToInterval on iOS. * Fixed Android *requiring* pagingEnabled to be defined alongside snapToInterval. * Added support for decelerationRate on Android. * Fixed snapping implementation. It was not calculating end position correctly at all (velocity is not a linear offset). * Resolves https://github.com/facebook/react-native/issues/20155 * Added support for new content being added during scroll (mirrors existing functionality in vertical ScrollView). * Added support for snapToInterval. * Resolves https://github.com/facebook/react-native/issues/19552 Reviewed By: yungsters Differential Revision: D9405703 fbshipit-source-id: b3c367b8079e6810794b0165dfdbcff4abff2eda
This commit is contained in:
parent
087e2a89fc
commit
fd744dd56c
@ -125,19 +125,6 @@ type IOSProps = $ReadOnly<{|
|
|||||||
* @platform ios
|
* @platform ios
|
||||||
*/
|
*/
|
||||||
centerContent?: ?boolean,
|
centerContent?: ?boolean,
|
||||||
/**
|
|
||||||
* A floating-point number that determines how quickly the scroll view
|
|
||||||
* decelerates after the user lifts their finger. You may also use string
|
|
||||||
* shortcuts `"normal"` and `"fast"` which match the underlying iOS settings
|
|
||||||
* for `UIScrollViewDecelerationRateNormal` and
|
|
||||||
* `UIScrollViewDecelerationRateFast` respectively.
|
|
||||||
*
|
|
||||||
* - `'normal'`: 0.998 (the default)
|
|
||||||
* - `'fast'`: 0.99
|
|
||||||
*
|
|
||||||
* @platform ios
|
|
||||||
*/
|
|
||||||
decelerationRate?: ?('fast' | 'normal' | number),
|
|
||||||
/**
|
/**
|
||||||
* The style of the scroll indicators.
|
* The style of the scroll indicators.
|
||||||
*
|
*
|
||||||
@ -353,6 +340,17 @@ export type Props = $ReadOnly<{|
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
contentContainerStyle?: ?ViewStyleProp,
|
contentContainerStyle?: ?ViewStyleProp,
|
||||||
|
/**
|
||||||
|
* A floating-point number that determines how quickly the scroll view
|
||||||
|
* decelerates after the user lifts their finger. You may also use string
|
||||||
|
* shortcuts `"normal"` and `"fast"` which match the underlying iOS settings
|
||||||
|
* for `UIScrollViewDecelerationRateNormal` and
|
||||||
|
* `UIScrollViewDecelerationRateFast` respectively.
|
||||||
|
*
|
||||||
|
* - `'normal'`: 0.998 on iOS, 0.985 on Android (the default)
|
||||||
|
* - `'fast'`: 0.99 on iOS, 0.9 on Android
|
||||||
|
*/
|
||||||
|
decelerationRate?: ?('fast' | 'normal' | number),
|
||||||
/**
|
/**
|
||||||
* When true, the scroll view's children are arranged horizontally in a row
|
* When true, the scroll view's children are arranged horizontally in a row
|
||||||
* instead of vertically in a column. The default value is false.
|
* instead of vertically in a column. The default value is false.
|
||||||
@ -462,12 +460,20 @@ export type Props = $ReadOnly<{|
|
|||||||
* When set, causes the scroll view to stop at multiples of the value of
|
* When set, causes the scroll view to stop at multiples of the value of
|
||||||
* `snapToInterval`. This can be used for paginating through children
|
* `snapToInterval`. This can be used for paginating through children
|
||||||
* that have lengths smaller than the scroll view. Typically used in
|
* that have lengths smaller than the scroll view. Typically used in
|
||||||
* combination with `snapToAlignment` and `decelerationRate="fast"` on ios.
|
* combination with `snapToAlignment` and `decelerationRate="fast"`.
|
||||||
* Overrides less configurable `pagingEnabled` prop.
|
|
||||||
*
|
*
|
||||||
* Supported for horizontal scrollview on android.
|
* Overrides less configurable `pagingEnabled` prop.
|
||||||
*/
|
*/
|
||||||
snapToInterval?: ?number,
|
snapToInterval?: ?number,
|
||||||
|
/**
|
||||||
|
* When set, causes the scroll view to stop at the defined offsets.
|
||||||
|
* This can be used for paginating through variously sized children
|
||||||
|
* that have lengths smaller than the scroll view. Typically used in
|
||||||
|
* combination with `decelerationRate="fast"`.
|
||||||
|
*
|
||||||
|
* Overrides less configurable `pagingEnabled` and `snapToInterval` props.
|
||||||
|
*/
|
||||||
|
snapToOffsets?: ?$ReadOnlyArray<number>,
|
||||||
/**
|
/**
|
||||||
* Experimental: When true, offscreen child views (whose `overflow` value is
|
* Experimental: When true, offscreen child views (whose `overflow` value is
|
||||||
* `hidden`) are removed from their native backing superview when offscreen.
|
* `hidden`) are removed from their native backing superview when offscreen.
|
||||||
@ -772,10 +778,6 @@ const ScrollView = createReactClass({
|
|||||||
} else {
|
} else {
|
||||||
ScrollViewClass = RCTScrollView;
|
ScrollViewClass = RCTScrollView;
|
||||||
ScrollContentContainerViewClass = RCTScrollContentView;
|
ScrollContentContainerViewClass = RCTScrollContentView;
|
||||||
warning(
|
|
||||||
this.props.snapToInterval == null || !this.props.pagingEnabled,
|
|
||||||
'snapToInterval is currently ignored when pagingEnabled is true.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
invariant(
|
invariant(
|
||||||
@ -919,6 +921,19 @@ const ScrollView = createReactClass({
|
|||||||
? true
|
? true
|
||||||
: false,
|
: false,
|
||||||
DEPRECATED_sendUpdatedChildFrames,
|
DEPRECATED_sendUpdatedChildFrames,
|
||||||
|
// pagingEnabled is overridden by snapToInterval / snapToOffsets
|
||||||
|
pagingEnabled: Platform.select({
|
||||||
|
// on iOS, pagingEnabled must be set to false to have snapToInterval / snapToOffsets work
|
||||||
|
ios:
|
||||||
|
this.props.pagingEnabled &&
|
||||||
|
this.props.snapToInterval == null &&
|
||||||
|
this.props.snapToOffsets == null,
|
||||||
|
// on Android, pagingEnabled must be set to true to have snapToInterval / snapToOffsets work
|
||||||
|
android:
|
||||||
|
this.props.pagingEnabled ||
|
||||||
|
this.props.snapToInterval != null ||
|
||||||
|
this.props.snapToOffsets != null,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const {decelerationRate} = this.props;
|
const {decelerationRate} = this.props;
|
||||||
|
@ -5,15 +5,26 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
* @format
|
* @format
|
||||||
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function processDecelerationRate(decelerationRate) {
|
const Platform = require('Platform');
|
||||||
|
|
||||||
|
function processDecelerationRate(
|
||||||
|
decelerationRate: number | 'normal' | 'fast',
|
||||||
|
): number {
|
||||||
if (decelerationRate === 'normal') {
|
if (decelerationRate === 'normal') {
|
||||||
decelerationRate = 0.998;
|
return Platform.select({
|
||||||
|
ios: 0.998,
|
||||||
|
android: 0.985,
|
||||||
|
});
|
||||||
} else if (decelerationRate === 'fast') {
|
} else if (decelerationRate === 'fast') {
|
||||||
decelerationRate = 0.99;
|
return Platform.select({
|
||||||
|
ios: 0.99,
|
||||||
|
android: 0.9,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return decelerationRate;
|
return decelerationRate;
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
@property (nonatomic, assign) BOOL centerContent;
|
@property (nonatomic, assign) BOOL centerContent;
|
||||||
@property (nonatomic, copy) NSDictionary *maintainVisibleContentPosition;
|
@property (nonatomic, copy) NSDictionary *maintainVisibleContentPosition;
|
||||||
@property (nonatomic, assign) int snapToInterval;
|
@property (nonatomic, assign) int snapToInterval;
|
||||||
|
@property (nonatomic, copy) NSArray<NSNumber *> *snapToOffsets;
|
||||||
@property (nonatomic, copy) NSString *snapToAlignment;
|
@property (nonatomic, copy) NSString *snapToAlignment;
|
||||||
|
|
||||||
// NOTE: currently these event props are only declared so we can export the
|
// NOTE: currently these event props are only declared so we can export the
|
||||||
|
@ -727,12 +727,72 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
|||||||
|
|
||||||
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
|
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
|
||||||
{
|
{
|
||||||
// snapToInterval
|
if (self.snapToOffsets) {
|
||||||
|
// An alternative to enablePaging and snapToInterval which allows setting custom
|
||||||
|
// stopping points that don't have to be the same distance apart. Often seen in
|
||||||
|
// apps which feature horizonally scrolling items. snapToInterval does not enforce
|
||||||
|
// scrolling one interval at a time but guarantees that the scroll will stop at
|
||||||
|
// a snap offset point.
|
||||||
|
|
||||||
|
// Find which axis to snap
|
||||||
|
BOOL isHorizontal = [self isHorizontal:scrollView];
|
||||||
|
|
||||||
|
// Calculate maximum content offset
|
||||||
|
CGSize viewportSize = [self _calculateViewportSize];
|
||||||
|
CGFloat maximumOffset = isHorizontal
|
||||||
|
? MAX(0, _scrollView.contentSize.width - viewportSize.width)
|
||||||
|
: MAX(0, _scrollView.contentSize.height - viewportSize.height);
|
||||||
|
|
||||||
|
// Calculate the snap offsets adjacent to the initial offset target
|
||||||
|
CGFloat targetOffset = isHorizontal ? targetContentOffset->x : targetContentOffset->y;
|
||||||
|
CGFloat smallerOffset = 0.0;
|
||||||
|
CGFloat largerOffset = maximumOffset;
|
||||||
|
|
||||||
|
for (int i = 0; i < self.snapToOffsets.count; i++) {
|
||||||
|
CGFloat offset = [[self.snapToOffsets objectAtIndex:i] floatValue];
|
||||||
|
|
||||||
|
if (offset <= targetOffset) {
|
||||||
|
if (targetOffset - offset < targetOffset - smallerOffset) {
|
||||||
|
smallerOffset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset >= targetOffset) {
|
||||||
|
if (offset - targetOffset < largerOffset - targetOffset) {
|
||||||
|
largerOffset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the nearest offset
|
||||||
|
CGFloat nearestOffset = targetOffset - smallerOffset < largerOffset - targetOffset
|
||||||
|
? smallerOffset
|
||||||
|
: largerOffset;
|
||||||
|
|
||||||
|
// Chose the correct snap offset based on velocity
|
||||||
|
CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
|
||||||
|
if (velocityAlongAxis > 0.0) {
|
||||||
|
targetOffset = largerOffset;
|
||||||
|
} else if (velocityAlongAxis < 0.0) {
|
||||||
|
targetOffset = smallerOffset;
|
||||||
|
} else {
|
||||||
|
targetOffset = nearestOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the new offset isn't out of bounds
|
||||||
|
targetOffset = MIN(MAX(0, targetOffset), maximumOffset);
|
||||||
|
|
||||||
|
// Set new targetContentOffset
|
||||||
|
if (isHorizontal) {
|
||||||
|
targetContentOffset->x = targetOffset;
|
||||||
|
} else {
|
||||||
|
targetContentOffset->y = targetOffset;
|
||||||
|
}
|
||||||
|
} else if (self.snapToInterval) {
|
||||||
// An alternative to enablePaging which allows setting custom stopping intervals,
|
// An alternative to enablePaging which allows setting custom stopping intervals,
|
||||||
// smaller than a full page size. Often seen in apps which feature horizonally
|
// smaller than a full page size. Often seen in apps which feature horizonally
|
||||||
// scrolling items. snapToInterval does not enforce scrolling one interval at a time
|
// scrolling items. snapToInterval does not enforce scrolling one interval at a time
|
||||||
// but guarantees that the scroll will stop at an interval point.
|
// but guarantees that the scroll will stop at an interval point.
|
||||||
if (self.snapToInterval) {
|
|
||||||
CGFloat snapToIntervalF = (CGFloat)self.snapToInterval;
|
CGFloat snapToIntervalF = (CGFloat)self.snapToInterval;
|
||||||
|
|
||||||
// Find which axis to snap
|
// Find which axis to snap
|
||||||
|
@ -81,6 +81,7 @@ RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
|
|||||||
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
|
RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
|
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(snapToOffsets, NSArray<NSNumber *>)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(snapToAlignment, NSString)
|
RCT_EXPORT_VIEW_PROPERTY(snapToAlignment, NSString)
|
||||||
RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset, CGPoint)
|
RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset, CGPoint)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onScrollBeginDrag, RCTDirectEventBlock)
|
RCT_EXPORT_VIEW_PROPERTY(onScrollBeginDrag, RCTDirectEventBlock)
|
||||||
|
@ -11,15 +11,20 @@ import android.annotation.TargetApi;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.support.v4.view.ViewCompat;
|
|
||||||
import android.graphics.drawable.LayerDrawable;
|
import android.graphics.drawable.LayerDrawable;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.hardware.SensorManager;
|
||||||
|
import android.support.v4.view.ViewCompat;
|
||||||
|
import android.support.v4.text.TextUtilsCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewConfiguration;
|
||||||
import android.widget.HorizontalScrollView;
|
import android.widget.HorizontalScrollView;
|
||||||
|
import android.widget.OverScroller;
|
||||||
|
|
||||||
import com.facebook.infer.annotation.Assertions;
|
import com.facebook.infer.annotation.Assertions;
|
||||||
import com.facebook.react.common.ReactConstants;
|
import com.facebook.react.common.ReactConstants;
|
||||||
import com.facebook.react.uimanager.MeasureSpecAssertions;
|
import com.facebook.react.uimanager.MeasureSpecAssertions;
|
||||||
@ -27,6 +32,10 @@ import com.facebook.react.uimanager.ReactClippingViewGroup;
|
|||||||
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
||||||
import com.facebook.react.uimanager.events.NativeGestureUtil;
|
import com.facebook.react.uimanager.events.NativeGestureUtil;
|
||||||
import com.facebook.react.views.view.ReactViewBackgroundManager;
|
import com.facebook.react.views.view.ReactViewBackgroundManager;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,7 +45,11 @@ import javax.annotation.Nullable;
|
|||||||
public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
||||||
ReactClippingViewGroup {
|
ReactClippingViewGroup {
|
||||||
|
|
||||||
|
private static @Nullable Field sScrollerField;
|
||||||
|
private static boolean sTriedToGetScrollerField = false;
|
||||||
|
|
||||||
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
|
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
|
||||||
|
private final @Nullable OverScroller mScroller;
|
||||||
private final VelocityHelper mVelocityHelper = new VelocityHelper();
|
private final VelocityHelper mVelocityHelper = new VelocityHelper();
|
||||||
private final Rect mRect = new Rect();
|
private final Rect mRect = new Rect();
|
||||||
|
|
||||||
@ -53,6 +66,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
private @Nullable Drawable mEndBackground;
|
private @Nullable Drawable mEndBackground;
|
||||||
private int mEndFillColor = Color.TRANSPARENT;
|
private int mEndFillColor = Color.TRANSPARENT;
|
||||||
private int mSnapInterval = 0;
|
private int mSnapInterval = 0;
|
||||||
|
private float mDecelerationRate = 0.985f;
|
||||||
|
private @Nullable List<Integer> mSnapOffsets;
|
||||||
private ReactViewBackgroundManager mReactBackgroundManager;
|
private ReactViewBackgroundManager mReactBackgroundManager;
|
||||||
|
|
||||||
public ReactHorizontalScrollView(Context context) {
|
public ReactHorizontalScrollView(Context context) {
|
||||||
@ -63,6 +78,47 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
super(context);
|
super(context);
|
||||||
mReactBackgroundManager = new ReactViewBackgroundManager(this);
|
mReactBackgroundManager = new ReactViewBackgroundManager(this);
|
||||||
mFpsListener = fpsListener;
|
mFpsListener = fpsListener;
|
||||||
|
|
||||||
|
mScroller = getOverScrollerFromParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private OverScroller getOverScrollerFromParent() {
|
||||||
|
OverScroller scroller;
|
||||||
|
|
||||||
|
if (!sTriedToGetScrollerField) {
|
||||||
|
sTriedToGetScrollerField = true;
|
||||||
|
try {
|
||||||
|
sScrollerField = HorizontalScrollView.class.getDeclaredField("mScroller");
|
||||||
|
sScrollerField.setAccessible(true);
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
Log.w(
|
||||||
|
ReactConstants.TAG,
|
||||||
|
"Failed to get mScroller field for HorizontalScrollView! " +
|
||||||
|
"This app will exhibit the bounce-back scrolling bug :(");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sScrollerField != null) {
|
||||||
|
try {
|
||||||
|
Object scrollerValue = sScrollerField.get(this);
|
||||||
|
if (scrollerValue instanceof OverScroller) {
|
||||||
|
scroller = (OverScroller) scrollerValue;
|
||||||
|
} else {
|
||||||
|
Log.w(
|
||||||
|
ReactConstants.TAG,
|
||||||
|
"Failed to cast mScroller field in HorizontalScrollView (probably due to OEM changes to AOSP)! " +
|
||||||
|
"This app will exhibit the bounce-back scrolling bug :(");
|
||||||
|
scroller = null;
|
||||||
|
}
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException("Failed to get mScroller from HorizontalScrollView!", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scroller = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scroller;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setScrollPerfTag(@Nullable String scrollPerfTag) {
|
public void setScrollPerfTag(@Nullable String scrollPerfTag) {
|
||||||
@ -95,10 +151,22 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
mPagingEnabled = pagingEnabled;
|
mPagingEnabled = pagingEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDecelerationRate(float decelerationRate) {
|
||||||
|
mDecelerationRate = decelerationRate;
|
||||||
|
|
||||||
|
if (mScroller != null) {
|
||||||
|
mScroller.setFriction(1.0f - mDecelerationRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setSnapInterval(int snapInterval) {
|
public void setSnapInterval(int snapInterval) {
|
||||||
mSnapInterval = snapInterval;
|
mSnapInterval = snapInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSnapOffsets(List<Integer> snapOffsets) {
|
||||||
|
mSnapOffsets = snapOffsets;
|
||||||
|
}
|
||||||
|
|
||||||
public void flashScrollIndicators() {
|
public void flashScrollIndicators() {
|
||||||
awakenScrollBars();
|
awakenScrollBars();
|
||||||
}
|
}
|
||||||
@ -194,7 +262,34 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
@Override
|
@Override
|
||||||
public void fling(int velocityX) {
|
public void fling(int velocityX) {
|
||||||
if (mPagingEnabled) {
|
if (mPagingEnabled) {
|
||||||
smoothScrollToPage(velocityX);
|
smoothScrollAndSnap(velocityX);
|
||||||
|
} else if (mScroller != null) {
|
||||||
|
// FB SCROLLVIEW CHANGE
|
||||||
|
|
||||||
|
// We provide our own version of fling that uses a different call to the standard OverScroller
|
||||||
|
// which takes into account the possibility of adding new content while the ScrollView is
|
||||||
|
// animating. Because we give essentially no max X for the fling, the fling will continue as long
|
||||||
|
// as there is content. See #onOverScrolled() to see the second part of this change which properly
|
||||||
|
// aborts the scroller animation when we get to the bottom of the ScrollView content.
|
||||||
|
|
||||||
|
int scrollWindowWidth = getWidth() - getPaddingStart() - getPaddingEnd();
|
||||||
|
|
||||||
|
mScroller.fling(
|
||||||
|
getScrollX(), // startX
|
||||||
|
getScrollY(), // startY
|
||||||
|
velocityX, // velocityX
|
||||||
|
0, // velocityY
|
||||||
|
0, // minX
|
||||||
|
Integer.MAX_VALUE, // maxX
|
||||||
|
0, // minY
|
||||||
|
0, // maxY
|
||||||
|
scrollWindowWidth / 2, // overX
|
||||||
|
0 // overY
|
||||||
|
);
|
||||||
|
|
||||||
|
ViewCompat.postInvalidateOnAnimation(this);
|
||||||
|
|
||||||
|
// END FB SCROLLVIEW CHANGE
|
||||||
} else {
|
} else {
|
||||||
super.fling(velocityX);
|
super.fling(velocityX);
|
||||||
}
|
}
|
||||||
@ -251,6 +346,28 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
|
||||||
|
if (mScroller != null) {
|
||||||
|
// FB SCROLLVIEW CHANGE
|
||||||
|
|
||||||
|
// This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for
|
||||||
|
// more information.
|
||||||
|
|
||||||
|
if (!mScroller.isFinished() && mScroller.getCurrX() != mScroller.getFinalX()) {
|
||||||
|
int scrollRange = computeHorizontalScrollRange() - getWidth();
|
||||||
|
if (scrollX >= scrollRange) {
|
||||||
|
mScroller.abortAnimation();
|
||||||
|
scrollX = scrollRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// END FB SCROLLVIEW CHANGE
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
|
||||||
|
}
|
||||||
|
|
||||||
private void enableFpsListener() {
|
private void enableFpsListener() {
|
||||||
if (isScrollPerfLoggingEnabled()) {
|
if (isScrollPerfLoggingEnabled()) {
|
||||||
Assertions.assertNotNull(mFpsListener);
|
Assertions.assertNotNull(mFpsListener);
|
||||||
@ -290,7 +407,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
* runnable that checks if we scrolled in the last frame and if so assumes we are still scrolling.
|
* runnable that checks if we scrolled in the last frame and if so assumes we are still scrolling.
|
||||||
*/
|
*/
|
||||||
private void handlePostTouchScrolling(int velocityX, int velocityY) {
|
private void handlePostTouchScrolling(int velocityX, int velocityY) {
|
||||||
// If we aren't going to do anything (send events or snap to page), we can early out.
|
// If we aren't going to do anything (send events or snap to page), we can early exit out.
|
||||||
if (!mSendMomentumEvents && !mPagingEnabled && !isScrollPerfLoggingEnabled()) {
|
if (!mSendMomentumEvents && !mPagingEnabled && !isScrollPerfLoggingEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -323,7 +440,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
// Only if we have pagingEnabled and we have not snapped to the page do we
|
// Only if we have pagingEnabled and we have not snapped to the page do we
|
||||||
// need to continue checking for the scroll. And we cause that scroll by asking for it
|
// need to continue checking for the scroll. And we cause that scroll by asking for it
|
||||||
mSnappingToPage = true;
|
mSnappingToPage = true;
|
||||||
smoothScrollToPage(0);
|
smoothScrollAndSnap(0);
|
||||||
ViewCompat.postOnAnimationDelayed(ReactHorizontalScrollView.this,
|
ViewCompat.postOnAnimationDelayed(ReactHorizontalScrollView.this,
|
||||||
this,
|
this,
|
||||||
ReactScrollViewHelper.MOMENTUM_DELAY);
|
ReactScrollViewHelper.MOMENTUM_DELAY);
|
||||||
@ -343,21 +460,124 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will smooth scroll us to the nearest page boundary
|
* This will smooth scroll us to the nearest snap offset point
|
||||||
* It currently just looks at where the content is relative to the page and slides to the nearest
|
* It currently just looks at where the content is and slides to the nearest point.
|
||||||
* page. It is intended to be run after we are done scrolling, and handling any momentum
|
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
|
||||||
* scrolling.
|
|
||||||
*/
|
*/
|
||||||
private void smoothScrollToPage(int velocity) {
|
private void smoothScrollAndSnap(int velocityX) {
|
||||||
int width = getSnapInterval();
|
if (getChildCount() <= 0) {
|
||||||
int currentX = getScrollX();
|
return;
|
||||||
// TODO (t11123799) - Should we do anything beyond linear accounting of the velocity
|
}
|
||||||
int predictedX = currentX + velocity;
|
|
||||||
int page = currentX / width;
|
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
|
||||||
if (predictedX > page * width + width / 2) {
|
int targetOffset = 0;
|
||||||
page = page + 1;
|
int smallerOffset = 0;
|
||||||
|
int largerOffset = maximumOffset;
|
||||||
|
|
||||||
|
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
|
||||||
|
// no way to customize the scroll duration. So, we create a temporary OverScroller
|
||||||
|
// so we can predict where a fling would land and snap to nearby that point.
|
||||||
|
OverScroller scroller = new OverScroller(getContext());
|
||||||
|
scroller.setFriction(1.0f - mDecelerationRate);
|
||||||
|
|
||||||
|
// predict where a fling would end up so we can scroll to the nearest snap offset
|
||||||
|
int width = getWidth() - getPaddingStart() - getPaddingEnd();
|
||||||
|
scroller.fling(
|
||||||
|
getScrollX(), // startX
|
||||||
|
getScrollY(), // startY
|
||||||
|
velocityX, // velocityX
|
||||||
|
0, // velocityY
|
||||||
|
0, // minX
|
||||||
|
maximumOffset, // maxX
|
||||||
|
0, // minY
|
||||||
|
0, // maxY
|
||||||
|
width/2, // overX
|
||||||
|
0 // overY
|
||||||
|
);
|
||||||
|
targetOffset = scroller.getFinalX();
|
||||||
|
|
||||||
|
// offsets are from the right edge in RTL layouts
|
||||||
|
boolean isRTL = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||||
|
if (isRTL) {
|
||||||
|
targetOffset = maximumOffset - targetOffset;
|
||||||
|
velocityX = -velocityX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the nearest snap points to the target offset
|
||||||
|
if (mSnapOffsets != null) {
|
||||||
|
for (int i = 0; i < mSnapOffsets.size(); i ++) {
|
||||||
|
int offset = mSnapOffsets.get(i);
|
||||||
|
|
||||||
|
if (offset <= targetOffset) {
|
||||||
|
if (targetOffset - offset < targetOffset - smallerOffset) {
|
||||||
|
smallerOffset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset >= targetOffset) {
|
||||||
|
if (offset - targetOffset < largerOffset - targetOffset) {
|
||||||
|
largerOffset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
double interval = (double) getSnapInterval();
|
||||||
|
double ratio = (double) targetOffset / interval;
|
||||||
|
smallerOffset = (int) (Math.floor(ratio) * interval);
|
||||||
|
largerOffset = (int) (Math.ceil(ratio) * interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the nearest offset
|
||||||
|
int nearestOffset = targetOffset - smallerOffset < largerOffset - targetOffset
|
||||||
|
? smallerOffset
|
||||||
|
: largerOffset;
|
||||||
|
|
||||||
|
// Chose the correct snap offset based on velocity
|
||||||
|
if (velocityX > 0) {
|
||||||
|
targetOffset = largerOffset;
|
||||||
|
} else if (velocityX < 0) {
|
||||||
|
targetOffset = smallerOffset;
|
||||||
|
} else {
|
||||||
|
targetOffset = nearestOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the new offset isn't out of bounds
|
||||||
|
targetOffset = Math.min(Math.max(0, targetOffset), maximumOffset);
|
||||||
|
|
||||||
|
if (isRTL) {
|
||||||
|
targetOffset = maximumOffset - targetOffset;
|
||||||
|
velocityX = -velocityX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// smoothScrollTo will always scroll over 250ms which is often *waaay*
|
||||||
|
// too short and will cause the scrolling to feel almost instant
|
||||||
|
// try to manually interact with OverScroller instead
|
||||||
|
// if velocity is 0 however, fling() won't work, so we want to use smoothScrollTo
|
||||||
|
if (mScroller != null) {
|
||||||
|
mActivelyScrolling = true;
|
||||||
|
|
||||||
|
mScroller.fling(
|
||||||
|
getScrollX(), // startX
|
||||||
|
getScrollY(), // startY
|
||||||
|
// velocity = 0 doesn't work with fling() so we pretend there's a reasonable
|
||||||
|
// initial velocity going on when a touch is released without any movement
|
||||||
|
velocityX != 0 ? velocityX : targetOffset - getScrollX(), // velocityX
|
||||||
|
0, // velocityY
|
||||||
|
// setting both minX and maxX to the same value will guarantee that we scroll to it
|
||||||
|
// but using the standard fling-style easing rather than smoothScrollTo's 250ms animation
|
||||||
|
targetOffset, // minX
|
||||||
|
targetOffset, // maxX
|
||||||
|
0, // minY
|
||||||
|
0, // maxY
|
||||||
|
// we only want to allow overscrolling if the final offset is at the very edge of the view
|
||||||
|
(targetOffset == 0 || targetOffset == maximumOffset) ? width / 2 : 0, // overX
|
||||||
|
0 // overY
|
||||||
|
);
|
||||||
|
|
||||||
|
postInvalidateOnAnimation();
|
||||||
|
} else {
|
||||||
|
smoothScrollTo(targetOffset, getScrollY());
|
||||||
}
|
}
|
||||||
smoothScrollTo(page * width, getScrollY());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -24,6 +24,8 @@ import com.facebook.react.uimanager.annotations.ReactProp;
|
|||||||
import com.facebook.react.uimanager.annotations.ReactPropGroup;
|
import com.facebook.react.uimanager.annotations.ReactPropGroup;
|
||||||
import com.facebook.yoga.YogaConstants;
|
import com.facebook.yoga.YogaConstants;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,6 +75,11 @@ public class ReactHorizontalScrollViewManager
|
|||||||
view.setHorizontalScrollBarEnabled(value);
|
view.setHorizontalScrollBarEnabled(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "decelerationRate")
|
||||||
|
public void setDecelerationRate(ReactHorizontalScrollView view, float decelerationRate) {
|
||||||
|
view.setDecelerationRate(decelerationRate);
|
||||||
|
}
|
||||||
|
|
||||||
@ReactProp(name = "snapToInterval")
|
@ReactProp(name = "snapToInterval")
|
||||||
public void setSnapToInterval(ReactHorizontalScrollView view, float snapToInterval) {
|
public void setSnapToInterval(ReactHorizontalScrollView view, float snapToInterval) {
|
||||||
// snapToInterval needs to be exposed as a float because of the Javascript interface.
|
// snapToInterval needs to be exposed as a float because of the Javascript interface.
|
||||||
@ -80,6 +87,16 @@ public class ReactHorizontalScrollViewManager
|
|||||||
view.setSnapInterval((int) (snapToInterval * screenDisplayMetrics.density));
|
view.setSnapInterval((int) (snapToInterval * screenDisplayMetrics.density));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "snapToOffsets")
|
||||||
|
public void setSnapToOffsets(ReactHorizontalScrollView view, @Nullable ReadableArray snapToOffsets) {
|
||||||
|
DisplayMetrics screenDisplayMetrics = DisplayMetricsHolder.getScreenDisplayMetrics();
|
||||||
|
List<Integer> offsets = new ArrayList<Integer>();
|
||||||
|
for (int i = 0; i < snapToOffsets.size(); i++) {
|
||||||
|
offsets.add((int) (snapToOffsets.getDouble(i) * screenDisplayMetrics.density));
|
||||||
|
}
|
||||||
|
view.setSnapOffsets(offsets);
|
||||||
|
}
|
||||||
|
|
||||||
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
|
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
|
||||||
public void setRemoveClippedSubviews(ReactHorizontalScrollView view, boolean removeClippedSubviews) {
|
public void setRemoveClippedSubviews(ReactHorizontalScrollView view, boolean removeClippedSubviews) {
|
||||||
view.setRemoveClippedSubviews(removeClippedSubviews);
|
view.setRemoveClippedSubviews(removeClippedSubviews);
|
||||||
|
@ -20,6 +20,7 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.OverScroller;
|
import android.widget.OverScroller;
|
||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
|
|
||||||
import com.facebook.infer.annotation.Assertions;
|
import com.facebook.infer.annotation.Assertions;
|
||||||
import com.facebook.react.bridge.ReactContext;
|
import com.facebook.react.bridge.ReactContext;
|
||||||
import com.facebook.react.common.ReactConstants;
|
import com.facebook.react.common.ReactConstants;
|
||||||
@ -28,7 +29,9 @@ import com.facebook.react.uimanager.ReactClippingViewGroup;
|
|||||||
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
||||||
import com.facebook.react.uimanager.events.NativeGestureUtil;
|
import com.facebook.react.uimanager.events.NativeGestureUtil;
|
||||||
import com.facebook.react.views.view.ReactViewBackgroundManager;
|
import com.facebook.react.views.view.ReactViewBackgroundManager;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,10 +52,11 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
private final VelocityHelper mVelocityHelper = new VelocityHelper();
|
private final VelocityHelper mVelocityHelper = new VelocityHelper();
|
||||||
private final Rect mRect = new Rect(); // for reuse to avoid allocation
|
private final Rect mRect = new Rect(); // for reuse to avoid allocation
|
||||||
|
|
||||||
|
private boolean mActivelyScrolling;
|
||||||
private @Nullable Rect mClippingRect;
|
private @Nullable Rect mClippingRect;
|
||||||
private boolean mDoneFlinging;
|
|
||||||
private boolean mDragging;
|
private boolean mDragging;
|
||||||
private boolean mFlinging;
|
private boolean mPagingEnabled = false;
|
||||||
|
private @Nullable Runnable mPostTouchRunnable;
|
||||||
private boolean mRemoveClippedSubviews;
|
private boolean mRemoveClippedSubviews;
|
||||||
private boolean mScrollEnabled = true;
|
private boolean mScrollEnabled = true;
|
||||||
private boolean mSendMomentumEvents;
|
private boolean mSendMomentumEvents;
|
||||||
@ -60,6 +64,9 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
private @Nullable String mScrollPerfTag;
|
private @Nullable String mScrollPerfTag;
|
||||||
private @Nullable Drawable mEndBackground;
|
private @Nullable Drawable mEndBackground;
|
||||||
private int mEndFillColor = Color.TRANSPARENT;
|
private int mEndFillColor = Color.TRANSPARENT;
|
||||||
|
private int mSnapInterval = 0;
|
||||||
|
private float mDecelerationRate = 0.985f;
|
||||||
|
private @Nullable List<Integer> mSnapOffsets;
|
||||||
private View mContentView;
|
private View mContentView;
|
||||||
private ReactViewBackgroundManager mReactBackgroundManager;
|
private ReactViewBackgroundManager mReactBackgroundManager;
|
||||||
|
|
||||||
@ -128,6 +135,26 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
mScrollEnabled = scrollEnabled;
|
mScrollEnabled = scrollEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPagingEnabled(boolean pagingEnabled) {
|
||||||
|
mPagingEnabled = pagingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDecelerationRate(float decelerationRate) {
|
||||||
|
mDecelerationRate = decelerationRate;
|
||||||
|
|
||||||
|
if (mScroller != null) {
|
||||||
|
mScroller.setFriction(1.0f - mDecelerationRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSnapInterval(int snapInterval) {
|
||||||
|
mSnapInterval = snapInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSnapOffsets(List<Integer> snapOffsets) {
|
||||||
|
mSnapOffsets = snapOffsets;
|
||||||
|
}
|
||||||
|
|
||||||
public void flashScrollIndicators() {
|
public void flashScrollIndicators() {
|
||||||
awakenScrollBars();
|
awakenScrollBars();
|
||||||
}
|
}
|
||||||
@ -167,15 +194,13 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
|
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
|
||||||
super.onScrollChanged(x, y, oldX, oldY);
|
super.onScrollChanged(x, y, oldX, oldY);
|
||||||
|
|
||||||
|
mActivelyScrolling = true;
|
||||||
|
|
||||||
if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
|
if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
|
||||||
if (mRemoveClippedSubviews) {
|
if (mRemoveClippedSubviews) {
|
||||||
updateClippingRect();
|
updateClippingRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFlinging) {
|
|
||||||
mDoneFlinging = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactScrollViewHelper.emitScrollEvent(
|
ReactScrollViewHelper.emitScrollEvent(
|
||||||
this,
|
this,
|
||||||
mOnScrollDispatchHelper.getXFlingVelocity(),
|
mOnScrollDispatchHelper.getXFlingVelocity(),
|
||||||
@ -216,12 +241,16 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
mVelocityHelper.calculateVelocity(ev);
|
mVelocityHelper.calculateVelocity(ev);
|
||||||
int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
||||||
if (action == MotionEvent.ACTION_UP && mDragging) {
|
if (action == MotionEvent.ACTION_UP && mDragging) {
|
||||||
|
float velocityX = mVelocityHelper.getXVelocity();
|
||||||
|
float velocityY = mVelocityHelper.getYVelocity();
|
||||||
ReactScrollViewHelper.emitScrollEndDragEvent(
|
ReactScrollViewHelper.emitScrollEndDragEvent(
|
||||||
this,
|
this,
|
||||||
mVelocityHelper.getXVelocity(),
|
velocityX,
|
||||||
mVelocityHelper.getYVelocity());
|
velocityY);
|
||||||
mDragging = false;
|
mDragging = false;
|
||||||
disableFpsListener();
|
// After the touch finishes, we may need to do some scrolling afterwards either as a result
|
||||||
|
// of a fling or because we need to page align the content
|
||||||
|
handlePostTouchScrolling(Math.round(velocityX), Math.round(velocityY));
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onTouchEvent(ev);
|
return super.onTouchEvent(ev);
|
||||||
@ -263,7 +292,9 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fling(int velocityY) {
|
public void fling(int velocityY) {
|
||||||
if (mScroller != null) {
|
if (mPagingEnabled) {
|
||||||
|
smoothScrollAndSnap(velocityY);
|
||||||
|
} else if (mScroller != null) {
|
||||||
// FB SCROLLVIEW CHANGE
|
// FB SCROLLVIEW CHANGE
|
||||||
|
|
||||||
// We provide our own version of fling that uses a different call to the standard OverScroller
|
// We provide our own version of fling that uses a different call to the standard OverScroller
|
||||||
@ -275,16 +306,17 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
|
int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
|
||||||
|
|
||||||
mScroller.fling(
|
mScroller.fling(
|
||||||
getScrollX(),
|
getScrollX(), // startX
|
||||||
getScrollY(),
|
getScrollY(), // startY
|
||||||
0,
|
0, // velocityX
|
||||||
velocityY,
|
velocityY, // velocityY
|
||||||
0,
|
0, // minX
|
||||||
0,
|
0, // maxX
|
||||||
0,
|
0, // minY
|
||||||
Integer.MAX_VALUE,
|
Integer.MAX_VALUE, // maxY
|
||||||
0,
|
0, // overX
|
||||||
scrollWindowHeight / 2);
|
scrollWindowHeight / 2 // overY
|
||||||
|
);
|
||||||
|
|
||||||
ViewCompat.postInvalidateOnAnimation(this);
|
ViewCompat.postInvalidateOnAnimation(this);
|
||||||
|
|
||||||
@ -292,29 +324,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
} else {
|
} else {
|
||||||
super.fling(velocityY);
|
super.fling(velocityY);
|
||||||
}
|
}
|
||||||
|
handlePostTouchScrolling(0, velocityY);
|
||||||
if (mSendMomentumEvents || isScrollPerfLoggingEnabled()) {
|
|
||||||
mFlinging = true;
|
|
||||||
enableFpsListener();
|
|
||||||
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this, 0, velocityY);
|
|
||||||
Runnable r = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (mDoneFlinging) {
|
|
||||||
mFlinging = false;
|
|
||||||
disableFpsListener();
|
|
||||||
ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactScrollView.this);
|
|
||||||
} else {
|
|
||||||
mDoneFlinging = true;
|
|
||||||
ViewCompat.postOnAnimationDelayed(
|
|
||||||
ReactScrollView.this,
|
|
||||||
this,
|
|
||||||
ReactScrollViewHelper.MOMENTUM_DELAY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ViewCompat.postOnAnimationDelayed(this, r, ReactScrollViewHelper.MOMENTUM_DELAY);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enableFpsListener() {
|
private void enableFpsListener() {
|
||||||
@ -357,6 +367,182 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
super.draw(canvas);
|
super.draw(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This handles any sort of scrolling that may occur after a touch is finished. This may be
|
||||||
|
* momentum scrolling (fling) or because you have pagingEnabled on the scroll view. Because we
|
||||||
|
* don't get any events from Android about this lifecycle, we do all our detection by creating a
|
||||||
|
* runnable that checks if we scrolled in the last frame and if so assumes we are still scrolling.
|
||||||
|
*/
|
||||||
|
private void handlePostTouchScrolling(int velocityX, int velocityY) {
|
||||||
|
// If we aren't going to do anything (send events or snap to page), we can early exit out.
|
||||||
|
if (!mSendMomentumEvents && !mPagingEnabled && !isScrollPerfLoggingEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we are already handling this which may occur if this is called by both the touch up
|
||||||
|
// and a fling call
|
||||||
|
if (mPostTouchRunnable != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mSendMomentumEvents) {
|
||||||
|
enableFpsListener();
|
||||||
|
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this, velocityX, velocityY);
|
||||||
|
}
|
||||||
|
|
||||||
|
mActivelyScrolling = false;
|
||||||
|
mPostTouchRunnable = new Runnable() {
|
||||||
|
|
||||||
|
private boolean mSnappingToPage = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (mActivelyScrolling) {
|
||||||
|
// We are still scrolling so we just post to check again a frame later
|
||||||
|
mActivelyScrolling = false;
|
||||||
|
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
|
||||||
|
this,
|
||||||
|
ReactScrollViewHelper.MOMENTUM_DELAY);
|
||||||
|
} else {
|
||||||
|
if (mPagingEnabled && !mSnappingToPage) {
|
||||||
|
// Only if we have pagingEnabled and we have not snapped to the page do we
|
||||||
|
// need to continue checking for the scroll. And we cause that scroll by asking for it
|
||||||
|
mSnappingToPage = true;
|
||||||
|
smoothScrollAndSnap(0);
|
||||||
|
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
|
||||||
|
this,
|
||||||
|
ReactScrollViewHelper.MOMENTUM_DELAY);
|
||||||
|
} else {
|
||||||
|
if (mSendMomentumEvents) {
|
||||||
|
ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactScrollView.this);
|
||||||
|
}
|
||||||
|
ReactScrollView.this.mPostTouchRunnable = null;
|
||||||
|
disableFpsListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
|
||||||
|
mPostTouchRunnable,
|
||||||
|
ReactScrollViewHelper.MOMENTUM_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will smooth scroll us to the nearest snap offset point
|
||||||
|
* It currently just looks at where the content is and slides to the nearest point.
|
||||||
|
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
|
||||||
|
*/
|
||||||
|
private void smoothScrollAndSnap(int velocityY) {
|
||||||
|
if (getChildCount() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int maximumOffset = getMaxScrollY();
|
||||||
|
int targetOffset = 0;
|
||||||
|
int smallerOffset = 0;
|
||||||
|
int largerOffset = maximumOffset;
|
||||||
|
|
||||||
|
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
|
||||||
|
// no way to customize the scroll duration. So, we create a temporary OverScroller
|
||||||
|
// so we can predict where a fling would land and snap to nearby that point.
|
||||||
|
OverScroller scroller = new OverScroller(getContext());
|
||||||
|
scroller.setFriction(1.0f - mDecelerationRate);
|
||||||
|
|
||||||
|
// predict where a fling would end up so we can scroll to the nearest snap offset
|
||||||
|
int height = getHeight() - getPaddingBottom() - getPaddingTop();
|
||||||
|
scroller.fling(
|
||||||
|
getScrollX(), // startX
|
||||||
|
getScrollY(), // startY
|
||||||
|
0, // velocityX
|
||||||
|
velocityY, // velocityY
|
||||||
|
0, // minX
|
||||||
|
0, // maxX
|
||||||
|
0, // minY
|
||||||
|
maximumOffset, // maxY
|
||||||
|
0, // overX
|
||||||
|
height/2 // overY
|
||||||
|
);
|
||||||
|
targetOffset = scroller.getFinalY();
|
||||||
|
|
||||||
|
// get the nearest snap points to the target offset
|
||||||
|
if (mSnapOffsets != null) {
|
||||||
|
for (int i = 0; i < mSnapOffsets.size(); i ++) {
|
||||||
|
int offset = mSnapOffsets.get(i);
|
||||||
|
|
||||||
|
if (offset <= targetOffset) {
|
||||||
|
if (targetOffset - offset < targetOffset - smallerOffset) {
|
||||||
|
smallerOffset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset >= targetOffset) {
|
||||||
|
if (offset - targetOffset < largerOffset - targetOffset) {
|
||||||
|
largerOffset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
double interval = (double) getSnapInterval();
|
||||||
|
double ratio = (double) targetOffset / interval;
|
||||||
|
smallerOffset = (int) (Math.floor(ratio) * interval);
|
||||||
|
largerOffset = (int) (Math.ceil(ratio) * interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the nearest offset
|
||||||
|
int nearestOffset = targetOffset - smallerOffset < largerOffset - targetOffset
|
||||||
|
? smallerOffset
|
||||||
|
: largerOffset;
|
||||||
|
|
||||||
|
// Chose the correct snap offset based on velocity
|
||||||
|
if (velocityY > 0) {
|
||||||
|
targetOffset = largerOffset;
|
||||||
|
} else if (velocityY < 0) {
|
||||||
|
targetOffset = smallerOffset;
|
||||||
|
} else {
|
||||||
|
targetOffset = nearestOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the new offset isn't out of bounds
|
||||||
|
targetOffset = Math.min(Math.max(0, targetOffset), maximumOffset);
|
||||||
|
|
||||||
|
// smoothScrollTo will always scroll over 250ms which is often *waaay*
|
||||||
|
// too short and will cause the scrolling to feel almost instant
|
||||||
|
// try to manually interact with OverScroller instead
|
||||||
|
// if velocity is 0 however, fling() won't work, so we want to use smoothScrollTo
|
||||||
|
if (mScroller != null) {
|
||||||
|
mActivelyScrolling = true;
|
||||||
|
|
||||||
|
mScroller.fling(
|
||||||
|
getScrollX(), // startX
|
||||||
|
getScrollY(), // startY
|
||||||
|
// velocity = 0 doesn't work with fling() so we pretend there's a reasonable
|
||||||
|
// initial velocity going on when a touch is released without any movement
|
||||||
|
0, // velocityX
|
||||||
|
velocityY != 0 ? velocityY : targetOffset - getScrollY(), // velocityY
|
||||||
|
0, // minX
|
||||||
|
0, // maxX
|
||||||
|
// setting both minY and maxY to the same value will guarantee that we scroll to it
|
||||||
|
// but using the standard fling-style easing rather than smoothScrollTo's 250ms animation
|
||||||
|
targetOffset, // minY
|
||||||
|
targetOffset, // maxY
|
||||||
|
0, // overX
|
||||||
|
// we only want to allow overscrolling if the final offset is at the very edge of the view
|
||||||
|
(targetOffset == 0 || targetOffset == maximumOffset) ? height / 2 : 0 // overY
|
||||||
|
);
|
||||||
|
|
||||||
|
postInvalidateOnAnimation();
|
||||||
|
} else {
|
||||||
|
smoothScrollTo(getScrollX(), targetOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getSnapInterval() {
|
||||||
|
if (mSnapInterval != 0) {
|
||||||
|
return mSnapInterval;
|
||||||
|
}
|
||||||
|
return getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
public void setEndFillColor(int color) {
|
public void setEndFillColor(int color) {
|
||||||
if (color != mEndFillColor) {
|
if (color != mEndFillColor) {
|
||||||
mEndFillColor = color;
|
mEndFillColor = color;
|
||||||
|
@ -10,10 +10,12 @@ package com.facebook.react.views.scroll;
|
|||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.support.v4.view.ViewCompat;
|
import android.support.v4.view.ViewCompat;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
|
||||||
import com.facebook.react.bridge.ReadableArray;
|
import com.facebook.react.bridge.ReadableArray;
|
||||||
import com.facebook.react.common.MapBuilder;
|
import com.facebook.react.common.MapBuilder;
|
||||||
import com.facebook.react.module.annotations.ReactModule;
|
import com.facebook.react.module.annotations.ReactModule;
|
||||||
|
import com.facebook.react.uimanager.DisplayMetricsHolder;
|
||||||
import com.facebook.react.uimanager.PixelUtil;
|
import com.facebook.react.uimanager.PixelUtil;
|
||||||
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
||||||
import com.facebook.react.uimanager.Spacing;
|
import com.facebook.react.uimanager.Spacing;
|
||||||
@ -24,13 +26,15 @@ import com.facebook.react.uimanager.annotations.ReactProp;
|
|||||||
import com.facebook.react.uimanager.annotations.ReactPropGroup;
|
import com.facebook.react.uimanager.annotations.ReactPropGroup;
|
||||||
import com.facebook.yoga.YogaConstants;
|
import com.facebook.yoga.YogaConstants;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View manager for {@link ReactScrollView} components.
|
* View manager for {@link ReactScrollView} components.
|
||||||
*
|
*
|
||||||
* <p>Note that {@link ReactScrollView} and {@link ReactHorizontalScrollView} are exposed to JS
|
* <p>Note that {@link ReactScrollView} and {@link ReactScrollView} are exposed to JS
|
||||||
* as a single ScrollView component, configured via the {@code horizontal} boolean property.
|
* as a single ScrollView component, configured via the {@code horizontal} boolean property.
|
||||||
*/
|
*/
|
||||||
@TargetApi(11)
|
@TargetApi(11)
|
||||||
@ -75,6 +79,28 @@ public class ReactScrollViewManager
|
|||||||
view.setVerticalScrollBarEnabled(value);
|
view.setVerticalScrollBarEnabled(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "decelerationRate")
|
||||||
|
public void setDecelerationRate(ReactScrollView view, float decelerationRate) {
|
||||||
|
view.setDecelerationRate(decelerationRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "snapToInterval")
|
||||||
|
public void setSnapToInterval(ReactScrollView view, float snapToInterval) {
|
||||||
|
// snapToInterval needs to be exposed as a float because of the Javascript interface.
|
||||||
|
DisplayMetrics screenDisplayMetrics = DisplayMetricsHolder.getScreenDisplayMetrics();
|
||||||
|
view.setSnapInterval((int) (snapToInterval * screenDisplayMetrics.density));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "snapToOffsets")
|
||||||
|
public void setSnapToOffsets(ReactScrollView view, @Nullable ReadableArray snapToOffsets) {
|
||||||
|
DisplayMetrics screenDisplayMetrics = DisplayMetricsHolder.getScreenDisplayMetrics();
|
||||||
|
List<Integer> offsets = new ArrayList<Integer>();
|
||||||
|
for (int i = 0; i < snapToOffsets.size(); i++) {
|
||||||
|
offsets.add((int) (snapToOffsets.getDouble(i) * screenDisplayMetrics.density));
|
||||||
|
}
|
||||||
|
view.setSnapOffsets(offsets);
|
||||||
|
}
|
||||||
|
|
||||||
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
|
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
|
||||||
public void setRemoveClippedSubviews(ReactScrollView view, boolean removeClippedSubviews) {
|
public void setRemoveClippedSubviews(ReactScrollView view, boolean removeClippedSubviews) {
|
||||||
view.setRemoveClippedSubviews(removeClippedSubviews);
|
view.setRemoveClippedSubviews(removeClippedSubviews);
|
||||||
@ -105,6 +131,11 @@ public class ReactScrollViewManager
|
|||||||
view.setScrollPerfTag(scrollPerfTag);
|
view.setScrollPerfTag(scrollPerfTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "pagingEnabled")
|
||||||
|
public void setPagingEnabled(ReactScrollView view, boolean pagingEnabled) {
|
||||||
|
view.setPagingEnabled(pagingEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When set, fills the rest of the scrollview with a color to avoid setting a background and
|
* When set, fills the rest of the scrollview with a color to avoid setting a background and
|
||||||
* creating unnecessary overdraw.
|
* creating unnecessary overdraw.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user