mirror of
https://github.com/status-im/react-native.git
synced 2025-02-26 08:05:34 +00:00
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:
parent
fd744dd56c
commit
5f48d28119
@ -474,6 +474,22 @@ export type Props = $ReadOnly<{|
|
|||||||
* Overrides less configurable `pagingEnabled` and `snapToInterval` props.
|
* Overrides less configurable `pagingEnabled` and `snapToInterval` props.
|
||||||
*/
|
*/
|
||||||
snapToOffsets?: ?$ReadOnlyArray<number>,
|
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
|
* 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.
|
||||||
@ -921,6 +937,10 @@ const ScrollView = createReactClass({
|
|||||||
? true
|
? true
|
||||||
: false,
|
: false,
|
||||||
DEPRECATED_sendUpdatedChildFrames,
|
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 is overridden by snapToInterval / snapToOffsets
|
||||||
pagingEnabled: Platform.select({
|
pagingEnabled: Platform.select({
|
||||||
// on iOS, pagingEnabled must be set to false to have snapToInterval / snapToOffsets work
|
// on iOS, pagingEnabled must be set to false to have snapToInterval / snapToOffsets work
|
||||||
|
@ -46,6 +46,8 @@
|
|||||||
@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) NSArray<NSNumber *> *snapToOffsets;
|
||||||
|
@property (nonatomic, assign) BOOL snapToStart;
|
||||||
|
@property (nonatomic, assign) BOOL snapToEnd;
|
||||||
@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
|
||||||
|
@ -736,6 +736,8 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
|||||||
|
|
||||||
// Find which axis to snap
|
// Find which axis to snap
|
||||||
BOOL isHorizontal = [self isHorizontal:scrollView];
|
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
|
// Calculate maximum content offset
|
||||||
CGSize viewportSize = [self _calculateViewportSize];
|
CGSize viewportSize = [self _calculateViewportSize];
|
||||||
@ -769,9 +771,26 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
|||||||
? smallerOffset
|
? smallerOffset
|
||||||
: largerOffset;
|
: largerOffset;
|
||||||
|
|
||||||
// Chose the correct snap offset based on velocity
|
CGFloat firstOffset = [[self.snapToOffsets firstObject] floatValue];
|
||||||
CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
|
CGFloat lastOffset = [[self.snapToOffsets lastObject] floatValue];
|
||||||
if (velocityAlongAxis > 0.0) {
|
|
||||||
|
// 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;
|
targetOffset = largerOffset;
|
||||||
} else if (velocityAlongAxis < 0.0) {
|
} else if (velocityAlongAxis < 0.0) {
|
||||||
targetOffset = smallerOffset;
|
targetOffset = smallerOffset;
|
||||||
|
@ -82,6 +82,8 @@ 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(snapToOffsets, NSArray<NSNumber *>)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(snapToStart, BOOL)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(snapToEnd, BOOL)
|
||||||
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)
|
||||||
|
@ -68,6 +68,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
private int mSnapInterval = 0;
|
private int mSnapInterval = 0;
|
||||||
private float mDecelerationRate = 0.985f;
|
private float mDecelerationRate = 0.985f;
|
||||||
private @Nullable List<Integer> mSnapOffsets;
|
private @Nullable List<Integer> mSnapOffsets;
|
||||||
|
private boolean mSnapToStart = true;
|
||||||
|
private boolean mSnapToEnd = true;
|
||||||
private ReactViewBackgroundManager mReactBackgroundManager;
|
private ReactViewBackgroundManager mReactBackgroundManager;
|
||||||
|
|
||||||
public ReactHorizontalScrollView(Context context) {
|
public ReactHorizontalScrollView(Context context) {
|
||||||
@ -167,6 +169,14 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
mSnapOffsets = snapOffsets;
|
mSnapOffsets = snapOffsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSnapToStart(boolean snapToStart) {
|
||||||
|
mSnapToStart = snapToStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSnapToEnd(boolean snapToEnd) {
|
||||||
|
mSnapToEnd = snapToEnd;
|
||||||
|
}
|
||||||
|
|
||||||
public void flashScrollIndicators() {
|
public void flashScrollIndicators() {
|
||||||
awakenScrollBars();
|
awakenScrollBars();
|
||||||
}
|
}
|
||||||
@ -473,6 +483,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
int targetOffset = 0;
|
int targetOffset = 0;
|
||||||
int smallerOffset = 0;
|
int smallerOffset = 0;
|
||||||
int largerOffset = maximumOffset;
|
int largerOffset = maximumOffset;
|
||||||
|
int firstOffset = 0;
|
||||||
|
int lastOffset = maximumOffset;
|
||||||
|
|
||||||
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
|
// 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
|
// 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
|
// get the nearest snap points to the target offset
|
||||||
if (mSnapOffsets != null) {
|
if (mSnapOffsets != null) {
|
||||||
|
firstOffset = mSnapOffsets.get(0);
|
||||||
|
lastOffset = mSnapOffsets.get(mSnapOffsets.size() - 1);
|
||||||
|
|
||||||
for (int i = 0; i < mSnapOffsets.size(); i ++) {
|
for (int i = 0; i < mSnapOffsets.size(); i ++) {
|
||||||
int offset = mSnapOffsets.get(i);
|
int offset = mSnapOffsets.get(i);
|
||||||
|
|
||||||
@ -532,10 +547,35 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
|||||||
? smallerOffset
|
? smallerOffset
|
||||||
: largerOffset;
|
: largerOffset;
|
||||||
|
|
||||||
// Chose the correct snap offset based on velocity
|
// if scrolling after the last snap offset and snapping to the
|
||||||
if (velocityX > 0) {
|
// 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;
|
targetOffset = largerOffset;
|
||||||
} else if (velocityX < 0) {
|
} else if (velocityX < 0) {
|
||||||
|
// when snapping velocity can feel sluggish for slow swipes
|
||||||
|
velocityX -= (int) ((targetOffset - smallerOffset) * 10.0);
|
||||||
|
|
||||||
targetOffset = smallerOffset;
|
targetOffset = smallerOffset;
|
||||||
} else {
|
} else {
|
||||||
targetOffset = nearestOffset;
|
targetOffset = nearestOffset;
|
||||||
|
@ -97,6 +97,16 @@ public class ReactHorizontalScrollViewManager
|
|||||||
view.setSnapOffsets(offsets);
|
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)
|
@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);
|
||||||
|
@ -67,6 +67,8 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
private int mSnapInterval = 0;
|
private int mSnapInterval = 0;
|
||||||
private float mDecelerationRate = 0.985f;
|
private float mDecelerationRate = 0.985f;
|
||||||
private @Nullable List<Integer> mSnapOffsets;
|
private @Nullable List<Integer> mSnapOffsets;
|
||||||
|
private boolean mSnapToStart = true;
|
||||||
|
private boolean mSnapToEnd = true;
|
||||||
private View mContentView;
|
private View mContentView;
|
||||||
private ReactViewBackgroundManager mReactBackgroundManager;
|
private ReactViewBackgroundManager mReactBackgroundManager;
|
||||||
|
|
||||||
@ -155,6 +157,14 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
mSnapOffsets = snapOffsets;
|
mSnapOffsets = snapOffsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSnapToStart(boolean snapToStart) {
|
||||||
|
mSnapToStart = snapToStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSnapToEnd(boolean snapToEnd) {
|
||||||
|
mSnapToEnd = snapToEnd;
|
||||||
|
}
|
||||||
|
|
||||||
public void flashScrollIndicators() {
|
public void flashScrollIndicators() {
|
||||||
awakenScrollBars();
|
awakenScrollBars();
|
||||||
}
|
}
|
||||||
@ -441,6 +451,8 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
int targetOffset = 0;
|
int targetOffset = 0;
|
||||||
int smallerOffset = 0;
|
int smallerOffset = 0;
|
||||||
int largerOffset = maximumOffset;
|
int largerOffset = maximumOffset;
|
||||||
|
int firstOffset = 0;
|
||||||
|
int lastOffset = maximumOffset;
|
||||||
|
|
||||||
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
|
// 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
|
// 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
|
// get the nearest snap points to the target offset
|
||||||
if (mSnapOffsets != null) {
|
if (mSnapOffsets != null) {
|
||||||
|
firstOffset = mSnapOffsets.get(0);
|
||||||
|
lastOffset = mSnapOffsets.get(mSnapOffsets.size() - 1);
|
||||||
|
|
||||||
for (int i = 0; i < mSnapOffsets.size(); i ++) {
|
for (int i = 0; i < mSnapOffsets.size(); i ++) {
|
||||||
int offset = mSnapOffsets.get(i);
|
int offset = mSnapOffsets.get(i);
|
||||||
|
|
||||||
@ -493,10 +508,31 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
|
|||||||
? smallerOffset
|
? smallerOffset
|
||||||
: largerOffset;
|
: largerOffset;
|
||||||
|
|
||||||
// Chose the correct snap offset based on velocity
|
// if scrolling after the last snap offset and snapping to the
|
||||||
if (velocityY > 0) {
|
// 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;
|
targetOffset = largerOffset;
|
||||||
} else if (velocityY < 0) {
|
} else if (velocityY < 0) {
|
||||||
|
// when snapping velocity can feel sluggish for slow swipes
|
||||||
|
velocityY -= (int) ((targetOffset - smallerOffset) * 10.0);
|
||||||
|
|
||||||
targetOffset = smallerOffset;
|
targetOffset = smallerOffset;
|
||||||
} else {
|
} else {
|
||||||
targetOffset = nearestOffset;
|
targetOffset = nearestOffset;
|
||||||
|
@ -101,6 +101,16 @@ public class ReactScrollViewManager
|
|||||||
view.setSnapOffsets(offsets);
|
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)
|
@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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user