mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Allow partially visible items to be picked as scroll anchors for maintainVisibleContentPosition
Summary: The current behavior for `maintainVisibleContentPosition` on ScrollView is to pick the first fully visible item as the scroll anchor. This has a number of disadvantages: * It causes problems for lists with loading indicators and large items. The loading glimmer can be picked as the anchor and pull the scroll down too quickly. This is the case for Marketplace. * It's inconsistent with the [CSS Scroll Anchoring](https://www.w3.org/TR/css-scroll-anchoring-1/) behavior, which is to pick the first partially visible view. This change will switch to picking the first partially visible view as the anchor, to align with the CSS implementation. Discussed the change with yungsters, NickGerleman, and cipolleschi and agreed about the change in behavior. This also enables `maintainVisibleContentPosition` for Android. After adding it to `validAttributes` for Android it appears to be working well. Previously it was not functional at all on Android, as the property change from React was not passed to ReactScrollViewManager.java. ## Changelog: [General] [Changed] - maintainVisibleContentPosition property on ScrollView now selects the first partially visible view as the anchor, rather than the first fully visible view. Reviewed By: NickGerleman Differential Revision: D54223244 fbshipit-source-id: 05ddfc0bbf16d61f9599b9d8066c0bd21b086301
This commit is contained in:
committed by
Facebook GitHub Bot
parent
32c74fbc59
commit
252ef19c8d
@@ -266,33 +266,6 @@ type IOSProps = $ReadOnly<{|
|
||||
* @platform ios
|
||||
*/
|
||||
canCancelContentTouches?: ?boolean,
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
maintainVisibleContentPosition?: ?$ReadOnly<{|
|
||||
minIndexForVisible: number,
|
||||
autoscrollToTopThreshold?: ?number,
|
||||
|}>,
|
||||
/**
|
||||
* The maximum allowed zoom scale. The default value is 1.0.
|
||||
* @platform ios
|
||||
@@ -505,6 +478,33 @@ export type Props = $ReadOnly<{|
|
||||
* - `true`, deprecated, use 'always' instead
|
||||
*/
|
||||
keyboardShouldPersistTaps?: ?('always' | 'never' | 'handled' | true | false),
|
||||
/**
|
||||
* When set, the scroll view will adjust the scroll position so that the first child that is
|
||||
* partially or fully 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
maintainVisibleContentPosition?: ?$ReadOnly<{|
|
||||
minIndexForVisible: number,
|
||||
autoscrollToTopThreshold?: ?number,
|
||||
|}>,
|
||||
/**
|
||||
* Called when the momentum scroll starts (scroll which occurs as the ScrollView glides to a stop).
|
||||
*/
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
|
||||
},
|
||||
decelerationRate: true,
|
||||
disableIntervalMomentum: true,
|
||||
maintainVisibleContentPosition: true,
|
||||
pagingEnabled: true,
|
||||
scrollEnabled: true,
|
||||
showsVerticalScrollIndicator: true,
|
||||
|
||||
+3
-3
@@ -752,13 +752,13 @@ static void RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrol
|
||||
BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width;
|
||||
int minIdx = props.maintainVisibleContentPosition.value().minIndexForVisible;
|
||||
for (NSUInteger ii = minIdx; ii < _contentView.subviews.count; ++ii) {
|
||||
// Find the first entirely visible view.
|
||||
// Find the first view that is partially or fully visible.
|
||||
UIView *subview = _contentView.subviews[ii];
|
||||
BOOL hasNewView = NO;
|
||||
if (horizontal) {
|
||||
hasNewView = subview.frame.origin.x > _scrollView.contentOffset.x;
|
||||
hasNewView = subview.frame.origin.x + subview.frame.size.width > _scrollView.contentOffset.x;
|
||||
} else {
|
||||
hasNewView = subview.frame.origin.y > _scrollView.contentOffset.y;
|
||||
hasNewView = subview.frame.origin.y + subview.frame.size.height > _scrollView.contentOffset.y;
|
||||
}
|
||||
if (hasNewView || ii == _contentView.subviews.count - 1) {
|
||||
_prevFirstVisibleFrame = subview.frame;
|
||||
|
||||
@@ -939,19 +939,19 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
|
||||
BOOL horz = [self isHorizontal:self->_scrollView];
|
||||
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
|
||||
// Find the first partially or fully 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
|
||||
UIView *subview = self->_contentView.subviews[ii];
|
||||
BOOL hasNewView = NO;
|
||||
if (horz) {
|
||||
CGFloat leftInset = self.inverted ? self->_scrollView.contentInset.right : self->_scrollView.contentInset.left;
|
||||
CGFloat x = self->_scrollView.contentOffset.x + leftInset;
|
||||
hasNewView = subview.frame.origin.x > x;
|
||||
hasNewView = subview.frame.origin.x + subview.frame.size.width > x;
|
||||
} else {
|
||||
CGFloat bottomInset =
|
||||
self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom;
|
||||
CGFloat y = self->_scrollView.contentOffset.y + bottomInset;
|
||||
hasNewView = subview.frame.origin.y > y;
|
||||
hasNewView = subview.frame.origin.y + subview.frame.size.height > y;
|
||||
}
|
||||
if (hasNewView || ii == self->_contentView.subviews.count - 1) {
|
||||
self->_prevFirstVisibleFrame = subview.frame;
|
||||
|
||||
+8
-1
@@ -34,6 +34,7 @@ import java.lang.ref.WeakReference;
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & HasSmoothScroll>
|
||||
implements UIManagerListener {
|
||||
|
||||
private final ScrollViewT mScrollView;
|
||||
private final boolean mHorizontal;
|
||||
private @Nullable Config mConfig;
|
||||
@@ -42,6 +43,7 @@ class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & HasSmo
|
||||
private boolean mListening = false;
|
||||
|
||||
public static class Config {
|
||||
|
||||
public final int minIndexForVisible;
|
||||
public final @Nullable Integer autoScrollToTopThreshold;
|
||||
|
||||
@@ -160,7 +162,12 @@ class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & HasSmo
|
||||
int currentScroll = mHorizontal ? mScrollView.getScrollX() : mScrollView.getScrollY();
|
||||
for (int i = mConfig.minIndexForVisible; i < contentView.getChildCount(); i++) {
|
||||
View child = contentView.getChildAt(i);
|
||||
float position = mHorizontal ? child.getX() : child.getY();
|
||||
|
||||
// Compute the position of the end of the child
|
||||
float position =
|
||||
mHorizontal ? child.getX() + child.getWidth() : child.getY() + child.getHeight();
|
||||
|
||||
// If the child is partially visible or this is the last child, select it as the anchor.
|
||||
if (position > currentScroll || i == contentView.getChildCount() - 1) {
|
||||
mFirstVisibleView = new WeakReference<>(child);
|
||||
Rect frame = new Rect();
|
||||
|
||||
Reference in New Issue
Block a user