Skip to content

Commit fd744dd

Browse files
olegblfacebook-github-bot
authored andcommitted
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 #20155 * Added support for new content being added during scroll (mirrors existing functionality in vertical ScrollView). * Added support for snapToInterval. * Resolves #19552 Reviewed By: yungsters Differential Revision: D9405703 fbshipit-source-id: b3c367b8079e6810794b0165dfdbcff4abff2eda
1 parent 087e2a8 commit fd744dd

File tree

9 files changed

+634
-92
lines changed

9 files changed

+634
-92
lines changed

Libraries/Components/ScrollView/ScrollView.js

+35-20
Original file line numberDiff line numberDiff line change
@@ -125,19 +125,6 @@ type IOSProps = $ReadOnly<{|
125125
* @platform ios
126126
*/
127127
centerContent?: ?boolean,
128-
/**
129-
* A floating-point number that determines how quickly the scroll view
130-
* decelerates after the user lifts their finger. You may also use string
131-
* shortcuts `"normal"` and `"fast"` which match the underlying iOS settings
132-
* for `UIScrollViewDecelerationRateNormal` and
133-
* `UIScrollViewDecelerationRateFast` respectively.
134-
*
135-
* - `'normal'`: 0.998 (the default)
136-
* - `'fast'`: 0.99
137-
*
138-
* @platform ios
139-
*/
140-
decelerationRate?: ?('fast' | 'normal' | number),
141128
/**
142129
* The style of the scroll indicators.
143130
*
@@ -353,6 +340,17 @@ export type Props = $ReadOnly<{|
353340
* ```
354341
*/
355342
contentContainerStyle?: ?ViewStyleProp,
343+
/**
344+
* A floating-point number that determines how quickly the scroll view
345+
* decelerates after the user lifts their finger. You may also use string
346+
* shortcuts `"normal"` and `"fast"` which match the underlying iOS settings
347+
* for `UIScrollViewDecelerationRateNormal` and
348+
* `UIScrollViewDecelerationRateFast` respectively.
349+
*
350+
* - `'normal'`: 0.998 on iOS, 0.985 on Android (the default)
351+
* - `'fast'`: 0.99 on iOS, 0.9 on Android
352+
*/
353+
decelerationRate?: ?('fast' | 'normal' | number),
356354
/**
357355
* When true, the scroll view's children are arranged horizontally in a row
358356
* instead of vertically in a column. The default value is false.
@@ -462,12 +460,20 @@ export type Props = $ReadOnly<{|
462460
* When set, causes the scroll view to stop at multiples of the value of
463461
* `snapToInterval`. This can be used for paginating through children
464462
* that have lengths smaller than the scroll view. Typically used in
465-
* combination with `snapToAlignment` and `decelerationRate="fast"` on ios.
466-
* Overrides less configurable `pagingEnabled` prop.
463+
* combination with `snapToAlignment` and `decelerationRate="fast"`.
467464
*
468-
* Supported for horizontal scrollview on android.
465+
* Overrides less configurable `pagingEnabled` prop.
469466
*/
470467
snapToInterval?: ?number,
468+
/**
469+
* When set, causes the scroll view to stop at the defined offsets.
470+
* This can be used for paginating through variously sized children
471+
* that have lengths smaller than the scroll view. Typically used in
472+
* combination with `decelerationRate="fast"`.
473+
*
474+
* Overrides less configurable `pagingEnabled` and `snapToInterval` props.
475+
*/
476+
snapToOffsets?: ?$ReadOnlyArray<number>,
471477
/**
472478
* Experimental: When true, offscreen child views (whose `overflow` value is
473479
* `hidden`) are removed from their native backing superview when offscreen.
@@ -772,10 +778,6 @@ const ScrollView = createReactClass({
772778
} else {
773779
ScrollViewClass = RCTScrollView;
774780
ScrollContentContainerViewClass = RCTScrollContentView;
775-
warning(
776-
this.props.snapToInterval == null || !this.props.pagingEnabled,
777-
'snapToInterval is currently ignored when pagingEnabled is true.',
778-
);
779781
}
780782

781783
invariant(
@@ -919,6 +921,19 @@ const ScrollView = createReactClass({
919921
? true
920922
: false,
921923
DEPRECATED_sendUpdatedChildFrames,
924+
// pagingEnabled is overridden by snapToInterval / snapToOffsets
925+
pagingEnabled: Platform.select({
926+
// on iOS, pagingEnabled must be set to false to have snapToInterval / snapToOffsets work
927+
ios:
928+
this.props.pagingEnabled &&
929+
this.props.snapToInterval == null &&
930+
this.props.snapToOffsets == null,
931+
// on Android, pagingEnabled must be set to true to have snapToInterval / snapToOffsets work
932+
android:
933+
this.props.pagingEnabled ||
934+
this.props.snapToInterval != null ||
935+
this.props.snapToOffsets != null,
936+
}),
922937
};
923938

924939
const {decelerationRate} = this.props;

Libraries/Components/ScrollView/processDecelerationRate.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,26 @@
55
* LICENSE file in the root directory of this source tree.
66
*
77
* @format
8+
* @flow
89
*/
910

1011
'use strict';
1112

12-
function processDecelerationRate(decelerationRate) {
13+
const Platform = require('Platform');
14+
15+
function processDecelerationRate(
16+
decelerationRate: number | 'normal' | 'fast',
17+
): number {
1318
if (decelerationRate === 'normal') {
14-
decelerationRate = 0.998;
19+
return Platform.select({
20+
ios: 0.998,
21+
android: 0.985,
22+
});
1523
} else if (decelerationRate === 'fast') {
16-
decelerationRate = 0.99;
24+
return Platform.select({
25+
ios: 0.99,
26+
android: 0.9,
27+
});
1728
}
1829
return decelerationRate;
1930
}

React/Views/ScrollView/RCTScrollView.h

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
@property (nonatomic, assign) BOOL centerContent;
4646
@property (nonatomic, copy) NSDictionary *maintainVisibleContentPosition;
4747
@property (nonatomic, assign) int snapToInterval;
48+
@property (nonatomic, copy) NSArray<NSNumber *> *snapToOffsets;
4849
@property (nonatomic, copy) NSString *snapToAlignment;
4950

5051
// NOTE: currently these event props are only declared so we can export the

React/Views/ScrollView/RCTScrollView.m

+66-6
Original file line numberDiff line numberDiff line change
@@ -727,12 +727,72 @@ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
727727

728728
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
729729
{
730-
// snapToInterval
731-
// An alternative to enablePaging which allows setting custom stopping intervals,
732-
// smaller than a full page size. Often seen in apps which feature horizonally
733-
// scrolling items. snapToInterval does not enforce scrolling one interval at a time
734-
// but guarantees that the scroll will stop at an interval point.
735-
if (self.snapToInterval) {
730+
if (self.snapToOffsets) {
731+
// An alternative to enablePaging and snapToInterval which allows setting custom
732+
// stopping points that don't have to be the same distance apart. Often seen in
733+
// apps which feature horizonally scrolling items. snapToInterval does not enforce
734+
// scrolling one interval at a time but guarantees that the scroll will stop at
735+
// a snap offset point.
736+
737+
// Find which axis to snap
738+
BOOL isHorizontal = [self isHorizontal:scrollView];
739+
740+
// Calculate maximum content offset
741+
CGSize viewportSize = [self _calculateViewportSize];
742+
CGFloat maximumOffset = isHorizontal
743+
? MAX(0, _scrollView.contentSize.width - viewportSize.width)
744+
: MAX(0, _scrollView.contentSize.height - viewportSize.height);
745+
746+
// Calculate the snap offsets adjacent to the initial offset target
747+
CGFloat targetOffset = isHorizontal ? targetContentOffset->x : targetContentOffset->y;
748+
CGFloat smallerOffset = 0.0;
749+
CGFloat largerOffset = maximumOffset;
750+
751+
for (int i = 0; i < self.snapToOffsets.count; i++) {
752+
CGFloat offset = [[self.snapToOffsets objectAtIndex:i] floatValue];
753+
754+
if (offset <= targetOffset) {
755+
if (targetOffset - offset < targetOffset - smallerOffset) {
756+
smallerOffset = offset;
757+
}
758+
}
759+
760+
if (offset >= targetOffset) {
761+
if (offset - targetOffset < largerOffset - targetOffset) {
762+
largerOffset = offset;
763+
}
764+
}
765+
}
766+
767+
// Calculate the nearest offset
768+
CGFloat nearestOffset = targetOffset - smallerOffset < largerOffset - targetOffset
769+
? smallerOffset
770+
: largerOffset;
771+
772+
// Chose the correct snap offset based on velocity
773+
CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
774+
if (velocityAlongAxis > 0.0) {
775+
targetOffset = largerOffset;
776+
} else if (velocityAlongAxis < 0.0) {
777+
targetOffset = smallerOffset;
778+
} else {
779+
targetOffset = nearestOffset;
780+
}
781+
782+
// Make sure the new offset isn't out of bounds
783+
targetOffset = MIN(MAX(0, targetOffset), maximumOffset);
784+
785+
// Set new targetContentOffset
786+
if (isHorizontal) {
787+
targetContentOffset->x = targetOffset;
788+
} else {
789+
targetContentOffset->y = targetOffset;
790+
}
791+
} else if (self.snapToInterval) {
792+
// An alternative to enablePaging which allows setting custom stopping intervals,
793+
// smaller than a full page size. Often seen in apps which feature horizonally
794+
// scrolling items. snapToInterval does not enforce scrolling one interval at a time
795+
// but guarantees that the scroll will stop at an interval point.
736796
CGFloat snapToIntervalF = (CGFloat)self.snapToInterval;
737797

738798
// Find which axis to snap

React/Views/ScrollView/RCTScrollViewManager.m

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ - (UIView *)view
8181
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
8282
RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
8383
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
84+
RCT_EXPORT_VIEW_PROPERTY(snapToOffsets, NSArray<NSNumber *>)
8485
RCT_EXPORT_VIEW_PROPERTY(snapToAlignment, NSString)
8586
RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset, CGPoint)
8687
RCT_EXPORT_VIEW_PROPERTY(onScrollBeginDrag, RCTDirectEventBlock)

0 commit comments

Comments
 (0)