rename and extend new maintain visible content position feature
Summary:
Builds off of cae7179c94
- Make the prop a dictionary for more configuration options
- Rename `maintainPositionAtOrBeyondIndex` -> `maintainVisibleContentPosition` + `minIndexForVisible`
- Add autoscroll threshold feature
Given the async native of RN JS and background layout, there is no way to trigger the scrollTo from JS without risking a delay, so we add the feature in native code.
== Test Plan ==
ScrollViewExample:
https://youtu.be/pmY8pxC9PRs
Reviewed By: shergin
Differential Revision: D6729160
fbshipit-source-id: 70f9bae460ce84567857a4f696da78ce9b3b834c
This commit is contained in:
parent
7e7d00aebe
commit
65184ec6b0
|
@ -234,18 +234,33 @@ const ScrollView = createReactClass({
|
|||
*/
|
||||
keyboardShouldPersistTaps: PropTypes.oneOf(['always', 'never', 'handled', false, true]),
|
||||
/**
|
||||
* When non-null, the scroll view will adjust the scroll position so that the content at or
|
||||
* beyond the specified index that is currently visible will not change position. This is useful
|
||||
* for lists that are loading content in both directions, e.g. a chat thread, where new messages
|
||||
* coming in might otherwise cause the scroll position to jump. A value of 1 can be used to skip
|
||||
* a spinner that does not need to maintain position. The default value is null.
|
||||
* When set, the scroll view will adjust the scroll position so that the first child that is
|
||||
* currently visible and at or beyond `minIndexForVisible` will not change position. This is
|
||||
* useful for lists that are loading content in both directions, e.g. a chat thread, where new
|
||||
* messages coming in might otherwise cause the scroll position to jump. A value of 0 is common,
|
||||
* but other values such as 1 can be used to skip loading spinners or other content that should
|
||||
* not maintain position.
|
||||
*
|
||||
* Caveat: reordering elements in the scrollview with this enabled will probably cause jumpiness
|
||||
* and jank. It can be fixed, but there are currently no plans to do so.
|
||||
* The optional `autoscrollToTopThreshold` can be used to make the content automatically scroll
|
||||
* to the top after making the adjustment if the user was within the threshold of the top before
|
||||
* the adjustment was made. This is also useful for chat-like applications where you want to see
|
||||
* new messages scroll into place, but not if the user has scrolled up a ways and it would be
|
||||
* disruptive to scroll a bunch.
|
||||
*
|
||||
* Caveat 1: Reordering elements in the scrollview with this enabled will probably cause
|
||||
* jumpiness and jank. It can be fixed, but there are currently no plans to do so. For now,
|
||||
* don't re-order the content of any ScrollViews or Lists that use this feature.
|
||||
*
|
||||
* Caveat 2: This simply uses `contentOffset` and `frame.origin` in native code to compute
|
||||
* visibility. Occlusion, transforms, and other complexity won't be taken into account as to
|
||||
* whether content is "visible" or not.
|
||||
*
|
||||
* @platform ios
|
||||
*/
|
||||
maintainPositionAtOrBeyondIndex: PropTypes.number,
|
||||
maintainVisibleContentPosition: PropTypes.shape({
|
||||
minIndexForVisible: PropTypes.number.isRequired,
|
||||
autoscrollToTopThreshold: PropTypes.number,
|
||||
}),
|
||||
/**
|
||||
* The maximum allowed zoom scale. The default value is 1.0.
|
||||
* @platform ios
|
||||
|
|
|
@ -131,7 +131,7 @@ if (Platform.OS === 'ios') {
|
|||
exports.examples.push({
|
||||
title: '<ScrollView> smooth bi-directional content loading\n',
|
||||
description:
|
||||
'The `maintainPositionAtOrBeyondIndex` prop allows insertions to either end of the content ' +
|
||||
'The `maintainVisibleContentPosition` prop allows insertions to either end of the content ' +
|
||||
'without causing the visible content to jump. Re-ordering is not supported.',
|
||||
render: function() {
|
||||
let itemCount = 6;
|
||||
|
@ -146,7 +146,10 @@ if (Platform.OS === 'ios') {
|
|||
<View>
|
||||
<ScrollView
|
||||
automaticallyAdjustContentInsets={false}
|
||||
maintainPositionAtOrBeyondIndex={1}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 1,
|
||||
autoscrollToTopThreshold: 10,
|
||||
}}
|
||||
style={styles.scrollView}>
|
||||
<ActivityIndicator style={{height: 40}} />
|
||||
{this.state.items.map(item =>
|
||||
|
@ -156,9 +159,12 @@ if (Platform.OS === 'ios') {
|
|||
<ScrollView
|
||||
horizontal={true}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
maintainPositionAtOrBeyondIndex={1}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 1,
|
||||
autoscrollToTopThreshold: 10,
|
||||
}}
|
||||
style={[styles.scrollView, styles.horizontalScrollView]}>
|
||||
<ActivityIndicator style={{height: 40}} />
|
||||
<ActivityIndicator style={{width: 40}} />
|
||||
{this.state.items.map(item =>
|
||||
React.cloneElement(item, {key: item.props.msg, style: null}),
|
||||
)}
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
@property (nonatomic, assign) BOOL DEPRECATED_sendUpdatedChildFrames;
|
||||
@property (nonatomic, assign) NSTimeInterval scrollEventThrottle;
|
||||
@property (nonatomic, assign) BOOL centerContent;
|
||||
@property (nonatomic, copy) NSNumber *maintainPositionAtOrBeyondIndex;
|
||||
@property (nonatomic, copy) NSDictionary *maintainVisibleContentPosition;
|
||||
@property (nonatomic, assign) int snapToInterval;
|
||||
@property (nonatomic, copy) NSString *snapToAlignment;
|
||||
|
||||
|
|
|
@ -911,16 +911,16 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
|||
}
|
||||
}
|
||||
|
||||
// maintainPositionAtOrBeyondIndex is used to allow seamless loading of content from both ends of
|
||||
// maintainVisibleContentPosition is used to allow seamless loading of content from both ends of
|
||||
// the scrollview without the visible content jumping in position.
|
||||
- (void)setMaintainPositionAtOrBeyondIndex:(NSNumber *)maintainPositionAtOrBeyondIndex
|
||||
- (void)setMaintainVisibleContentPosition:(NSDictionary *)maintainVisibleContentPosition
|
||||
{
|
||||
if (maintainPositionAtOrBeyondIndex != nil) {
|
||||
if (maintainVisibleContentPosition != nil && _maintainVisibleContentPosition == nil) {
|
||||
[_eventDispatcher.bridge.uiManager.observerCoordinator addObserver:self];
|
||||
} else {
|
||||
} else if (maintainVisibleContentPosition == nil && _maintainVisibleContentPosition != nil) {
|
||||
[_eventDispatcher.bridge.uiManager.observerCoordinator removeObserver:self];
|
||||
}
|
||||
_maintainPositionAtOrBeyondIndex = maintainPositionAtOrBeyondIndex;
|
||||
_maintainVisibleContentPosition = maintainVisibleContentPosition;
|
||||
}
|
||||
|
||||
#pragma mark - RCTUIManagerObserver
|
||||
|
@ -930,7 +930,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
|||
RCTAssertUIManagerQueue();
|
||||
[manager prependUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
BOOL horz = [self isHorizontal:self->_scrollView];
|
||||
NSUInteger minIdx = [self->_maintainPositionAtOrBeyondIndex integerValue];
|
||||
NSUInteger minIdx = [self->_maintainVisibleContentPosition[@"minIndexForVisible"] integerValue];
|
||||
for (NSUInteger ii = minIdx; ii < self->_contentView.subviews.count; ++ii) {
|
||||
// Find the first entirely visible view. This must be done after we update the content offset
|
||||
// or it will tend to grab rows that were made visible by the shift in position
|
||||
|
@ -946,9 +946,10 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
|||
}
|
||||
}];
|
||||
[manager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
if (self->_maintainPositionAtOrBeyondIndex == nil) {
|
||||
if (self->_maintainVisibleContentPosition == nil) {
|
||||
return; // The prop might have changed in the previous UIBlocks, so need to abort here.
|
||||
}
|
||||
NSNumber *autoscrollThreshold = self->_maintainVisibleContentPosition[@"autoscrollToTopThreshold"];
|
||||
// TODO: detect and handle/ignore re-ordering
|
||||
if ([self isHorizontal:self->_scrollView]) {
|
||||
CGFloat deltaX = self->_firstVisibleView.frame.origin.x - self->_prevFirstVisibleFrame.origin.x;
|
||||
|
@ -957,15 +958,27 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
|||
self->_scrollView.contentOffset.x + deltaX,
|
||||
self->_scrollView.contentOffset.y
|
||||
);
|
||||
if (autoscrollThreshold != nil) {
|
||||
// If the offset WAS within the threshold of the start, animate to the start.
|
||||
if (self->_scrollView.contentOffset.x - deltaX <= [autoscrollThreshold integerValue]) {
|
||||
[self scrollToOffset:CGPointMake(0, self->_scrollView.contentOffset.y) animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CGRect newFrame = self->_firstVisibleView.frame;
|
||||
CGFloat deltaY = newFrame.origin.y - self->_prevFirstVisibleFrame.origin.y;
|
||||
if (ABS(deltaY) > 0.1 || deltaY != 0.0) {
|
||||
if (ABS(deltaY) > 0.1) {
|
||||
self->_scrollView.contentOffset = CGPointMake(
|
||||
self->_scrollView.contentOffset.x,
|
||||
self->_scrollView.contentOffset.y + deltaY
|
||||
);
|
||||
if (autoscrollThreshold != nil) {
|
||||
// If the offset WAS within the threshold of the start, animate to the start.
|
||||
if (self->_scrollView.contentOffset.y - deltaY <= [autoscrollThreshold integerValue]) {
|
||||
[self scrollToOffset:CGPointMake(self->_scrollView.contentOffset.x, 0) animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
|
|
@ -62,7 +62,7 @@ RCT_EXPORT_VIEW_PROPERTY(bounces, BOOL)
|
|||
RCT_EXPORT_VIEW_PROPERTY(bouncesZoom, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(canCancelContentTouches, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(centerContent, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(maintainPositionAtOrBeyondIndex, NSNumber)
|
||||
RCT_EXPORT_VIEW_PROPERTY(maintainVisibleContentPosition, NSDictionary)
|
||||
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(decelerationRate, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(directionalLockEnabled, BOOL)
|
||||
|
|
Loading…
Reference in New Issue