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 facebook-github-bot committed Aug 30, 2018
1 parent 087e2a8 commit fd744dd
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 @@ -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(
Expand Down Expand Up @@ -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;
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

6 comments on commit fd744dd

@brunolemos
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you ❤

@harrisrobin
Copy link

Choose a reason for hiding this comment

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

Amazing!

@mannol
Copy link
Contributor

@mannol mannol commented on fd744dd Sep 25, 2018

Choose a reason for hiding this comment

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

<3

@thecryptovalley
Copy link

Choose a reason for hiding this comment

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

Thanks for solving this out. How can this I use this commit with React Native 0.56?

@petetastic
Copy link

Choose a reason for hiding this comment

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

Thanks for solving this out. How can this I use this commit with React Native 0.56?

Hi @thecryptovalley did you manage to figure out how to get this commit working with 0.56?

@pribeh
Copy link

@pribeh pribeh commented on fd744dd Jan 22, 2019

Choose a reason for hiding this comment

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

Anybody get this working on .56 or earlier?

Please sign in to comment.