ScrollView snapToStart/snapToEnd

Summary: Added `snapToStart` and `snapToEnd` props to ScrollView which work together with `snapToOffsets` and determine whether the beginning and end of the list automatically count as snap offsets or not. If not, the list is allowed to free-scroll between its start/end and the first/last snap offset.

Reviewed By: sahrens

Differential Revision: D9442386

fbshipit-source-id: 47a5fdb20f884542434b01b1f0a486ed2b478c6e
This commit is contained in:
Oleg Lokhvitsky 2018-08-30 12:59:37 -07:00 committed by Facebook Github Bot
parent fd744dd56c
commit 5f48d28119
8 changed files with 146 additions and 7 deletions

View File

@ -474,6 +474,22 @@ export type Props = $ReadOnly<{|
* Overrides less configurable `pagingEnabled` and `snapToInterval` props.
*/
snapToOffsets?: ?$ReadOnlyArray<number>,
/**
* Use in conjuction with `snapToOffsets`. By default, the beginning
* of the list counts as a snap offset. Set `snapToStart` to false to disable
* this behavior and allow the list to scroll freely between its start and
* the first `snapToOffsets` offset.
* The default value is true.
*/
snapToStart?: ?boolean,
/**
* Use in conjuction with `snapToOffsets`. By default, the end
* of the list counts as a snap offset. Set `snapToEnd` to false to disable
* this behavior and allow the list to scroll freely between its end and
* the last `snapToOffsets` offset.
* The default value is true.
*/
snapToEnd?: ?boolean,
/**
* Experimental: When true, offscreen child views (whose `overflow` value is
* `hidden`) are removed from their native backing superview when offscreen.
@ -921,6 +937,10 @@ const ScrollView = createReactClass({
? true
: false,
DEPRECATED_sendUpdatedChildFrames,
// default to true
snapToStart: this.props.snapToStart !== false,
// default to true
snapToEnd: this.props.snapToEnd !== false,
// pagingEnabled is overridden by snapToInterval / snapToOffsets
pagingEnabled: Platform.select({
// on iOS, pagingEnabled must be set to false to have snapToInterval / snapToOffsets work

View File

@ -46,6 +46,8 @@
@property (nonatomic, copy) NSDictionary *maintainVisibleContentPosition;
@property (nonatomic, assign) int snapToInterval;
@property (nonatomic, copy) NSArray<NSNumber *> *snapToOffsets;
@property (nonatomic, assign) BOOL snapToStart;
@property (nonatomic, assign) BOOL snapToEnd;
@property (nonatomic, copy) NSString *snapToAlignment;
// NOTE: currently these event props are only declared so we can export the

View File

@ -736,6 +736,8 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
// Find which axis to snap
BOOL isHorizontal = [self isHorizontal:scrollView];
CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
CGFloat offsetAlongAxis = isHorizontal ? _scrollView.contentOffset.x : _scrollView.contentOffset.y;
// Calculate maximum content offset
CGSize viewportSize = [self _calculateViewportSize];
@ -769,9 +771,26 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
? smallerOffset
: largerOffset;
// Chose the correct snap offset based on velocity
CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
if (velocityAlongAxis > 0.0) {
CGFloat firstOffset = [[self.snapToOffsets firstObject] floatValue];
CGFloat lastOffset = [[self.snapToOffsets lastObject] floatValue];
// if scrolling after the last snap offset and snapping to the
// end of the list is disabled, then we allow free scrolling
if (!self.snapToEnd && targetOffset >= lastOffset) {
if (offsetAlongAxis >= lastOffset) {
// free scrolling
} else {
// snap to end
targetOffset = lastOffset;
}
} else if (!self.snapToStart && targetOffset <= firstOffset) {
if (offsetAlongAxis <= firstOffset) {
// free scrolling
} else {
// snap to beginning
targetOffset = firstOffset;
}
} else if (velocityAlongAxis > 0.0) {
targetOffset = largerOffset;
} else if (velocityAlongAxis < 0.0) {
targetOffset = smallerOffset;

View File

@ -82,6 +82,8 @@ 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(snapToStart, BOOL)
RCT_EXPORT_VIEW_PROPERTY(snapToEnd, BOOL)
RCT_EXPORT_VIEW_PROPERTY(snapToAlignment, NSString)
RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset, CGPoint)
RCT_EXPORT_VIEW_PROPERTY(onScrollBeginDrag, RCTDirectEventBlock)

View File

@ -68,6 +68,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
private int mSnapInterval = 0;
private float mDecelerationRate = 0.985f;
private @Nullable List<Integer> mSnapOffsets;
private boolean mSnapToStart = true;
private boolean mSnapToEnd = true;
private ReactViewBackgroundManager mReactBackgroundManager;
public ReactHorizontalScrollView(Context context) {
@ -167,6 +169,14 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
mSnapOffsets = snapOffsets;
}
public void setSnapToStart(boolean snapToStart) {
mSnapToStart = snapToStart;
}
public void setSnapToEnd(boolean snapToEnd) {
mSnapToEnd = snapToEnd;
}
public void flashScrollIndicators() {
awakenScrollBars();
}
@ -473,6 +483,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
int targetOffset = 0;
int smallerOffset = 0;
int largerOffset = maximumOffset;
int firstOffset = 0;
int lastOffset = 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
@ -505,6 +517,9 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
// get the nearest snap points to the target offset
if (mSnapOffsets != null) {
firstOffset = mSnapOffsets.get(0);
lastOffset = mSnapOffsets.get(mSnapOffsets.size() - 1);
for (int i = 0; i < mSnapOffsets.size(); i ++) {
int offset = mSnapOffsets.get(i);
@ -532,10 +547,35 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
? smallerOffset
: largerOffset;
// Chose the correct snap offset based on velocity
if (velocityX > 0) {
// if scrolling after the last snap offset and snapping to the
// end of the list is disabled, then we allow free scrolling
int currentOffset = getScrollX();
if (isRTL) {
currentOffset = maximumOffset - currentOffset;
}
if (!mSnapToEnd && targetOffset >= lastOffset) {
if (currentOffset >= lastOffset) {
// free scrolling
} else {
// snap to end
targetOffset = lastOffset;
}
} else if (!mSnapToStart && targetOffset <= firstOffset) {
if (currentOffset <= firstOffset) {
// free scrolling
} else {
// snap to beginning
targetOffset = firstOffset;
}
} else if (velocityX > 0) {
// when snapping velocity can feel sluggish for slow swipes
velocityX += (int) ((largerOffset - targetOffset) * 10.0);
targetOffset = largerOffset;
} else if (velocityX < 0) {
// when snapping velocity can feel sluggish for slow swipes
velocityX -= (int) ((targetOffset - smallerOffset) * 10.0);
targetOffset = smallerOffset;
} else {
targetOffset = nearestOffset;

View File

@ -97,6 +97,16 @@ public class ReactHorizontalScrollViewManager
view.setSnapOffsets(offsets);
}
@ReactProp(name = "snapToStart")
public void setSnapToStart(ReactHorizontalScrollView view, boolean snapToStart) {
view.setSnapToStart(snapToStart);
}
@ReactProp(name = "snapToEnd")
public void setSnapToEnd(ReactHorizontalScrollView view, boolean snapToEnd) {
view.setSnapToEnd(snapToEnd);
}
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
public void setRemoveClippedSubviews(ReactHorizontalScrollView view, boolean removeClippedSubviews) {
view.setRemoveClippedSubviews(removeClippedSubviews);

View File

@ -67,6 +67,8 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
private int mSnapInterval = 0;
private float mDecelerationRate = 0.985f;
private @Nullable List<Integer> mSnapOffsets;
private boolean mSnapToStart = true;
private boolean mSnapToEnd = true;
private View mContentView;
private ReactViewBackgroundManager mReactBackgroundManager;
@ -155,6 +157,14 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
mSnapOffsets = snapOffsets;
}
public void setSnapToStart(boolean snapToStart) {
mSnapToStart = snapToStart;
}
public void setSnapToEnd(boolean snapToEnd) {
mSnapToEnd = snapToEnd;
}
public void flashScrollIndicators() {
awakenScrollBars();
}
@ -441,6 +451,8 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
int targetOffset = 0;
int smallerOffset = 0;
int largerOffset = maximumOffset;
int firstOffset = 0;
int lastOffset = 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
@ -466,6 +478,9 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
// get the nearest snap points to the target offset
if (mSnapOffsets != null) {
firstOffset = mSnapOffsets.get(0);
lastOffset = mSnapOffsets.get(mSnapOffsets.size() - 1);
for (int i = 0; i < mSnapOffsets.size(); i ++) {
int offset = mSnapOffsets.get(i);
@ -493,10 +508,31 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
? smallerOffset
: largerOffset;
// Chose the correct snap offset based on velocity
if (velocityY > 0) {
// if scrolling after the last snap offset and snapping to the
// end of the list is disabled, then we allow free scrolling
if (!mSnapToEnd && targetOffset >= lastOffset) {
if (getScrollY() >= lastOffset) {
// free scrolling
} else {
// snap to end
targetOffset = lastOffset;
}
} else if (!mSnapToStart && targetOffset <= firstOffset) {
if (getScrollY() <= firstOffset) {
// free scrolling
} else {
// snap to beginning
targetOffset = firstOffset;
}
} else if (velocityY > 0) {
// when snapping velocity can feel sluggish for slow swipes
velocityY += (int) ((largerOffset - targetOffset) * 10.0);
targetOffset = largerOffset;
} else if (velocityY < 0) {
// when snapping velocity can feel sluggish for slow swipes
velocityY -= (int) ((targetOffset - smallerOffset) * 10.0);
targetOffset = smallerOffset;
} else {
targetOffset = nearestOffset;

View File

@ -101,6 +101,16 @@ public class ReactScrollViewManager
view.setSnapOffsets(offsets);
}
@ReactProp(name = "snapToStart")
public void setSnapToStart(ReactScrollView view, boolean snapToStart) {
view.setSnapToStart(snapToStart);
}
@ReactProp(name = "snapToEnd")
public void setSnapToEnd(ReactScrollView view, boolean snapToEnd) {
view.setSnapToEnd(snapToEnd);
}
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
public void setRemoveClippedSubviews(ReactScrollView view, boolean removeClippedSubviews) {
view.setRemoveClippedSubviews(removeClippedSubviews);