Permalink
Browse files

Definable distance pagination for ScrollView

Summary: This is an enhancement for ScrollView that adds the ability to paginate based on a width other than the width of the ScrollView itself. This is a fairly common pattern used on apps like Facebook, App Store, and Twitter to scroll through a horizontal set of cards or icons:

![img_8726 2](https://cloud.githubusercontent.com/assets/451050/8017899/39f9f47c-0bd2-11e5-9c1d-889452f20cf7.PNG) ![img_8727 2](https://cloud.githubusercontent.com/assets/451050/8017898/39f962dc-0bd2-11e5-98b4-461ac0f7f21b.PNG)  ![img_8728 2](https://cloud.githubusercontent.com/assets/451050/8017900/39fd91a4-0bd2-11e5-8786-4cf0316295a0.PNG)

After trying to accomplish this only with JS, it appears that attempting to take over an in-progress native scroll animation with JS is always going to result in some amount of jankiness and jumping.

This pull request uses `scrollViewWillEndDragging` in RCTScrollView.m to adjust `targetContentOffset` based on two new optional props added to ScrollView. `snapToInterval` sets the multiple that the
Closes #1532

Reviewed By: @​svcscm, @​trunkagent

Differential Revision: D2443701

Pulled By: @vjeux
  • Loading branch information...
rxb authored and facebook-github-bot-7 committed Sep 23, 2015
1 parent b9ef384 commit dcf245a9a2061d4aa1e6e68fe3e2ad4e33d8f9e5
@@ -246,6 +246,27 @@ var ScrollView = React.createClass({
*/
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),
style: StyleSheetPropType(ViewStylePropTypes),
/**
* 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. Used in combination
* with `snapToAlignment`.
* @platform ios
*/
snapToInterval: PropTypes.number,
/**
* When `snapToInterval` is set, `snapToAlignment` will define the relationship
* of the the snapping to the scroll view.
* - `start` (the default) will align the snap at the left (horizontal) or top (vertical)
* - `center` will align the snap in the center
* - `end` will align the snap at the right (horizontal) or bottom (vertical)
* @platform ios
*/
snapToAlignment: PropTypes.oneOf([
'start', // default
'center',
'end',
]),
/**
* Experimental: When true, offscreen child views (whose `overflow` value is
* `hidden`) are removed from their native backing superview when offscreen.
@@ -430,6 +451,8 @@ var validAttributes = {
scrollsToTop: true,
showsHorizontalScrollIndicator: true,
showsVerticalScrollIndicator: true,
snapToInterval: true,
snapToAlignment: true,
stickyHeaderIndices: {diff: deepDiffer},
scrollEventThrottle: true,
zoomScale: true,
@@ -44,6 +44,8 @@
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
@property (nonatomic, assign) NSTimeInterval scrollEventThrottle;
@property (nonatomic, assign) BOOL centerContent;
@property (nonatomic, assign) int snapToInterval;
@property (nonatomic, copy) NSString *snapToAlignment;
@property (nonatomic, copy) NSIndexSet *stickyHeaderIndices;
@end
@@ -620,6 +620,48 @@ - (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) {
CGFloat snapToIntervalF = (CGFloat)self.snapToInterval;
// Find which axis to snap
BOOL isHorizontal = (scrollView.contentSize.width > self.frame.size.width);
// What is the current offset?
CGFloat targetContentOffsetAlongAxis = isHorizontal ? targetContentOffset->x : targetContentOffset->y;
// Which direction is the scroll travelling?
CGPoint translation = [scrollView.panGestureRecognizer translationInView:scrollView];
CGFloat translationAlongAxis = isHorizontal ? translation.x : translation.y;
// Offset based on desired alignment
CGFloat frameLength = isHorizontal ? self.frame.size.width : self.frame.size.height;
CGFloat alignmentOffset = 0.0f;
if ([self.snapToAlignment isEqualToString: @"center"]) {
alignmentOffset = (frameLength * 0.5f) + (snapToIntervalF * 0.5f);
} else if ([self.snapToAlignment isEqualToString: @"end"]) {
alignmentOffset = frameLength;
}
// Pick snap point based on direction and proximity
NSInteger snapIndex = floor((targetContentOffsetAlongAxis + alignmentOffset) / snapToIntervalF);
snapIndex = (translationAlongAxis < 0) ? snapIndex + 1 : snapIndex;
CGFloat newTargetContentOffset = ( snapIndex * snapToIntervalF ) - alignmentOffset;
// Set new targetContentOffset
if (isHorizontal) {
targetContentOffset->x = newTargetContentOffset;
} else {
targetContentOffset->y = newTargetContentOffset;
}
}
NSDictionary *userData = @{
@"velocity": @{
@"x": @(velocity.x),
@@ -631,6 +673,7 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi
}
};
[_eventDispatcher sendScrollEventWithType:RCTScrollEventTypeEnd reactTag:self.reactTag scrollView:scrollView userData:userData];
RCT_FORWARD_SCROLL_EVENT(scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset);
}
@@ -63,6 +63,8 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
RCT_EXPORT_VIEW_PROPERTY(snapToAlignment, NSString)
RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset, CGPoint)
- (NSDictionary *)constantsToExport

1 comment on commit dcf245a

@chirag04

This comment has been minimized.

Show comment
Hide comment
@chirag04

chirag04 Sep 23, 2015

Collaborator

👍

Collaborator

chirag04 commented on dcf245a Sep 23, 2015

👍

Please sign in to comment.