Skip to content

Commit

Permalink
ScrollView snapToOffsets
Browse files Browse the repository at this point in the history
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
  • Loading branch information
olegbl authored and grabbou committed Aug 31, 2018
1 parent a3b2f5d commit ef7e99c
Show file tree
Hide file tree
Showing 9 changed files with 634 additions and 92 deletions.
55 changes: 35 additions & 20 deletions Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -773,10 +779,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(
Expand Down Expand Up @@ -920,6 +922,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;
Expand Down
17 changes: 14 additions & 3 deletions Libraries/Components/ScrollView/processDecelerationRate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions React/Views/ScrollView/RCTScrollView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 66 additions & 6 deletions React/Views/ScrollView/RCTScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -727,12 +727,72 @@ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView

- (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
Expand Down
1 change: 1 addition & 0 deletions React/Views/ScrollView/RCTScrollViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ - (UIView *)view
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)
Expand Down
Loading

1 comment on commit ef7e99c

@henrikra
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very good update!

Please sign in to comment.