Permalink
Browse files

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
  • Loading branch information...
olegbl authored and grabbou committed Aug 30, 2018
1 parent a3b2f5d commit ef7e99c1bbc7f3a99093961bc5e7717c9e45dc79
@@ -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.
*
@@ -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.
@@ -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.
@@ -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(
@@ -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;
@@ -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;
}
@@ -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
@@ -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
@@ -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)
Oops, something went wrong.

1 comment on commit ef7e99c

@henrikra

This comment has been minimized.

Show comment
Hide comment
@henrikra

henrikra Sep 13, 2018

This is very good update!

henrikra commented on ef7e99c Sep 13, 2018

This is very good update!

Please sign in to comment.