diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 3a87d05a8a4296..a7c88726fe70e9 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -210,14 +210,45 @@ var ScrollView = React.createClass({ return React.findNodeHandle(this.refs[INNERVIEW]); }, - scrollTo: function(destY?: number, destX?: number) { + scrollTo: function(destY?: number, destX?: number, options?: { animated?: bool, duration?: number, easing?: string }) { if (Platform.OS === 'android') { + // TODO: update to allow for custom duration which is already enabled on iOS RCTUIManager.dispatchViewManagerCommand( React.findNodeHandle(this), RCTUIManager.RCTScrollView.Commands.scrollTo, [destX || 0, destY || 0] ); } else { + var animated = options && options.animated || !options; + + if (!animated) { + // scroll with no animation + RCTUIManager.scrollWithoutAnimationTo( + React.findNodeHandle(this), + destX || 0, + destY || 0 + ); + return; + } + + // scroll using RCTAnimation with custom duration if a duration was passed + if (animated && (options && options.duration)) { + invariant( + options.duration >= 0, + 'Duration passed to ScrollView \'scrollTo\' ' + + 'should be greater than or equal to 0.' + ); + RCTUIManager.scrollWithCustomDurationTo( + React.findNodeHandle(this), + destX || 0, + destY || 0, + options.duration, + options.easing || 'linear' + ); + return; + } + + // Default scroll behavior RCTUIManager.scrollTo( React.findNodeHandle(this), destX || 0, @@ -227,11 +258,7 @@ var ScrollView = React.createClass({ }, scrollWithoutAnimationTo: function(destY?: number, destX?: number) { - RCTUIManager.scrollWithoutAnimationTo( - React.findNodeHandle(this), - destX || 0, - destY || 0 - ); + this.scrollTo(destY, destX, { animated: false }); }, render: function() { diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 7d6925b120da67..3296e474216939 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -1122,6 +1122,26 @@ static void RCTMeasureLayout(RCTShadowView *view, }]; } +RCT_EXPORT_METHOD(scrollWithCustomDurationTo:(NSNumber *)reactTag + offsetX:(CGFloat)offsetX + offsetY:(CGFloat)offsetY + duration:(NSTimeInterval)duration + animationType:(RCTAnimationType)animationType) +{ + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ + UIView *view = viewRegistry[reactTag]; + if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { + RCTAnimation *animation = [[RCTAnimation alloc] initWithDuration:duration dictionary:@{ @"type": @(animationType) }]; + + [animation performAnimations:^{ + [(id)view scrollToOffset:(CGPoint){offsetX, offsetY} animated:NO]; + } withCompletionBlock:nil]; + } else { + RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); + } + }]; +} + RCT_EXPORT_METHOD(zoomToRect:(NSNumber *)reactTag withRect:(CGRect)rect) {