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
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -353,6 +340,17 @@ export type Props = $ReadOnly<{|
|
||||
* ```
|
||||
*/
|
||||
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
|
||||
* 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
|
||||
* `snapToInterval`. This can be used for paginating through children
|
||||
* that have lengths smaller than the scroll view. Typically used in
|
||||
* combination with `snapToAlignment` and `decelerationRate="fast"` on ios.
|
||||
* Overrides less configurable `pagingEnabled` prop.
|
||||
* combination with `snapToAlignment` and `decelerationRate="fast"`.
|
||||
*
|
||||
* Supported for horizontal scrollview on android.
|
||||
* Overrides less configurable `pagingEnabled` prop.
|
||||
*/
|
||||
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
|
||||
* `hidden`) are removed from their native backing superview when offscreen.
|
||||
@ -772,10 +778,6 @@ const ScrollView = createReactClass({
|
||||
} else {
|
||||
ScrollViewClass = RCTScrollView;
|
||||
ScrollContentContainerViewClass = RCTScrollContentView;
|
||||
warning(
|
||||
this.props.snapToInterval == null || !this.props.pagingEnabled,
|
||||
'snapToInterval is currently ignored when pagingEnabled is true.',
|
||||
);
|
||||
}
|
||||
|
||||
invariant(
|
||||
@ -919,6 +921,19 @@ const ScrollView = createReactClass({
|
||||
? true
|
||||
: false,
|
||||
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;
|
||||
|
@ -5,15 +5,26 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
function processDecelerationRate(decelerationRate) {
|
||||
const Platform = require('Platform');
|
||||
|
||||
function processDecelerationRate(
|
||||
decelerationRate: number | 'normal' | 'fast',
|
||||
): number {
|
||||
if (decelerationRate === 'normal') {
|
||||
decelerationRate = 0.998;
|
||||
return Platform.select({
|
||||
ios: 0.998,
|
||||
android: 0.985,
|
||||
});
|
||||
} else if (decelerationRate === 'fast') {
|
||||
decelerationRate = 0.99;
|
||||
return Platform.select({
|
||||
ios: 0.99,
|
||||
android: 0.9,
|
||||
});
|
||||
}
|
||||
return decelerationRate;
|
||||
}
|
||||
|
@ -45,6 +45,7 @@
|
||||
@property (nonatomic, assign) BOOL centerContent;
|
||||
@property (nonatomic, copy) NSDictionary *maintainVisibleContentPosition;
|
||||
@property (nonatomic, assign) int snapToInterval;
|
||||
@property (nonatomic, copy) NSArray<NSNumber *> *snapToOffsets;
|
||||
@property (nonatomic, copy) NSString *snapToAlignment;
|
||||
|
||||
// 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
|
||||
{
|
||||
// snapToInterval
|
||||
// An alternative to enablePaging which allows setting custom stopping intervals,
|
||||
// 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
|
||||
// but guarantees that the scroll will stop at an interval point.
|
||||
if (self.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,
|
||||
// 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
|
||||
// but guarantees that the scroll will stop at an interval point.
|
||||
CGFloat snapToIntervalF = (CGFloat)self.snapToInterval;
|
||||
|
||||
// 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(scrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
|
||||
RCT_EXPORT_VIEW_PROPERTY(snapToOffsets, NSArray<NSNumber *>)
|
||||
RCT_EXPORT_VIEW_PROPERTY(snapToAlignment, NSString)
|
||||
RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset, CGPoint)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onScrollBeginDrag, RCTDirectEventBlock)
|
||||
|
@ -11,15 +11,20 @@ import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
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.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.OverScroller;
|
||||
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
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.events.NativeGestureUtil;
|
||||
import com.facebook.react.views.view.ReactViewBackgroundManager;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
@ -36,7 +45,11 @@ import javax.annotation.Nullable;
|
||||
public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
||||
ReactClippingViewGroup {
|
||||
|
||||
private static @Nullable Field sScrollerField;
|
||||
private static boolean sTriedToGetScrollerField = false;
|
||||
|
||||
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
|
||||
private final @Nullable OverScroller mScroller;
|
||||
private final VelocityHelper mVelocityHelper = new VelocityHelper();
|
||||
private final Rect mRect = new Rect();
|
||||
|
||||
@ -53,6 +66,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
||||
private @Nullable Drawable mEndBackground;
|
||||
private int mEndFillColor = Color.TRANSPARENT;
|
||||
private int mSnapInterval = 0;
|
||||
private float mDecelerationRate = 0.985f;
|
||||
private @Nullable List<Integer> mSnapOffsets;
|
||||
private ReactViewBackgroundManager mReactBackgroundManager;
|
||||
|
||||
public ReactHorizontalScrollView(Context context) {
|
||||
@ -63,6 +78,47 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
||||
super(context);
|
||||
mReactBackgroundManager = new ReactViewBackgroundManager(this);
|
||||
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) {
|
||||
@ -95,10 +151,22 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
||||
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() {
|
||||
awakenScrollBars();
|
||||
}
|
||||
@ -194,7 +262,34 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
||||
@Override
|
||||
public void fling(int velocityX) {
|
||||
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 {
|
||||
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() {
|
||||
if (isScrollPerfLoggingEnabled()) {
|
||||
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.
|
||||
*/
|
||||
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()) {
|
||||
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
|
||||
// need to continue checking for the scroll. And we cause that scroll by asking for it
|
||||
mSnappingToPage = true;
|
||||
smoothScrollToPage(0);
|
||||
smoothScrollAndSnap(0);
|
||||
ViewCompat.postOnAnimationDelayed(ReactHorizontalScrollView.this,
|
||||
this,
|
||||
ReactScrollViewHelper.MOMENTUM_DELAY);
|
||||
@ -343,21 +460,124 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
||||
}
|
||||
|
||||
/**
|
||||
* This will smooth scroll us to the nearest page boundary
|
||||
* It currently just looks at where the content is relative to the page and slides to the nearest
|
||||
* page. It is intended to be run after we are done scrolling, and handling any momentum
|
||||
* scrolling.
|
||||
* 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 smoothScrollToPage(int velocity) {
|
||||
int width = getSnapInterval();
|
||||
int currentX = getScrollX();
|
||||
// TODO (t11123799) - Should we do anything beyond linear accounting of the velocity
|
||||
int predictedX = currentX + velocity;
|
||||
int page = currentX / width;
|
||||
if (predictedX > page * width + width / 2) {
|
||||
page = page + 1;
|
||||
private void smoothScrollAndSnap(int velocityX) {
|
||||
if (getChildCount() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
|
||||
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 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
|
||||
|
@ -24,6 +24,8 @@ import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.uimanager.annotations.ReactPropGroup;
|
||||
import com.facebook.yoga.YogaConstants;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
@ -73,6 +75,11 @@ public class ReactHorizontalScrollViewManager
|
||||
view.setHorizontalScrollBarEnabled(value);
|
||||
}
|
||||
|
||||
@ReactProp(name = "decelerationRate")
|
||||
public void setDecelerationRate(ReactHorizontalScrollView view, float decelerationRate) {
|
||||
view.setDecelerationRate(decelerationRate);
|
||||
}
|
||||
|
||||
@ReactProp(name = "snapToInterval")
|
||||
public void setSnapToInterval(ReactHorizontalScrollView view, float snapToInterval) {
|
||||
// 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));
|
||||
}
|
||||
|
||||
@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)
|
||||
public void setRemoveClippedSubviews(ReactHorizontalScrollView view, boolean removeClippedSubviews) {
|
||||
view.setRemoveClippedSubviews(removeClippedSubviews);
|
||||
|
@ -20,6 +20,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.OverScroller;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
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.events.NativeGestureUtil;
|
||||
import com.facebook.react.views.view.ReactViewBackgroundManager;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
@ -49,10 +52,11 @@ 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 boolean mActivelyScrolling;
|
||||
private @Nullable Rect mClippingRect;
|
||||
private boolean mDoneFlinging;
|
||||
private boolean mDragging;
|
||||
private boolean mFlinging;
|
||||
private boolean mPagingEnabled = false;
|
||||
private @Nullable Runnable mPostTouchRunnable;
|
||||
private boolean mRemoveClippedSubviews;
|
||||
private boolean mScrollEnabled = true;
|
||||
private boolean mSendMomentumEvents;
|
||||
@ -60,6 +64,9 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
||||
private @Nullable String mScrollPerfTag;
|
||||
private @Nullable Drawable mEndBackground;
|
||||
private int mEndFillColor = Color.TRANSPARENT;
|
||||
private int mSnapInterval = 0;
|
||||
private float mDecelerationRate = 0.985f;
|
||||
private @Nullable List<Integer> mSnapOffsets;
|
||||
private View mContentView;
|
||||
private ReactViewBackgroundManager mReactBackgroundManager;
|
||||
|
||||
@ -128,6 +135,26 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
||||
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() {
|
||||
awakenScrollBars();
|
||||
}
|
||||
@ -167,15 +194,13 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
||||
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
|
||||
super.onScrollChanged(x, y, oldX, oldY);
|
||||
|
||||
mActivelyScrolling = true;
|
||||
|
||||
if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
|
||||
if (mRemoveClippedSubviews) {
|
||||
updateClippingRect();
|
||||
}
|
||||
|
||||
if (mFlinging) {
|
||||
mDoneFlinging = false;
|
||||
}
|
||||
|
||||
ReactScrollViewHelper.emitScrollEvent(
|
||||
this,
|
||||
mOnScrollDispatchHelper.getXFlingVelocity(),
|
||||
@ -216,12 +241,16 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
||||
mVelocityHelper.calculateVelocity(ev);
|
||||
int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
||||
if (action == MotionEvent.ACTION_UP && mDragging) {
|
||||
float velocityX = mVelocityHelper.getXVelocity();
|
||||
float velocityY = mVelocityHelper.getYVelocity();
|
||||
ReactScrollViewHelper.emitScrollEndDragEvent(
|
||||
this,
|
||||
mVelocityHelper.getXVelocity(),
|
||||
mVelocityHelper.getYVelocity());
|
||||
velocityX,
|
||||
velocityY);
|
||||
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);
|
||||
@ -263,7 +292,9 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
||||
|
||||
@Override
|
||||
public void fling(int velocityY) {
|
||||
if (mScroller != null) {
|
||||
if (mPagingEnabled) {
|
||||
smoothScrollAndSnap(velocityY);
|
||||
} else if (mScroller != null) {
|
||||
// FB SCROLLVIEW CHANGE
|
||||
|
||||
// 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();
|
||||
|
||||
mScroller.fling(
|
||||
getScrollX(),
|
||||
getScrollY(),
|
||||
0,
|
||||
velocityY,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
Integer.MAX_VALUE,
|
||||
0,
|
||||
scrollWindowHeight / 2);
|
||||
getScrollX(), // startX
|
||||
getScrollY(), // startY
|
||||
0, // velocityX
|
||||
velocityY, // velocityY
|
||||
0, // minX
|
||||
0, // maxX
|
||||
0, // minY
|
||||
Integer.MAX_VALUE, // maxY
|
||||
0, // overX
|
||||
scrollWindowHeight / 2 // overY
|
||||
);
|
||||
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
|
||||
@ -292,29 +324,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
||||
} else {
|
||||
super.fling(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);
|
||||
}
|
||||
handlePostTouchScrolling(0, velocityY);
|
||||
}
|
||||
|
||||
private void enableFpsListener() {
|
||||
@ -357,6 +367,182 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
||||
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) {
|
||||
if (color != mEndFillColor) {
|
||||
mEndFillColor = color;
|
||||
|
@ -10,10 +10,12 @@ package com.facebook.react.views.scroll;
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Color;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.uimanager.DisplayMetricsHolder;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
||||
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.yoga.YogaConstants;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@TargetApi(11)
|
||||
@ -75,6 +79,28 @@ public class ReactScrollViewManager
|
||||
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)
|
||||
public void setRemoveClippedSubviews(ReactScrollView view, boolean removeClippedSubviews) {
|
||||
view.setRemoveClippedSubviews(removeClippedSubviews);
|
||||
@ -105,6 +131,11 @@ public class ReactScrollViewManager
|
||||
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
|
||||
* creating unnecessary overdraw.
|
||||
|
Loading…
x
Reference in New Issue
Block a user