diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index e497288b1b7d..ed0b9f45618e 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -255,34 +255,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. - * - * @platform ios - */ - maintainVisibleContentPosition?: ?$ReadOnly<{| - minIndexForVisible: number, - autoscrollToTopThreshold?: ?number, - |}>, /** * The maximum allowed zoom scale. The default value is 1.0. * @platform ios @@ -519,6 +491,32 @@ 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 + * 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, + |}>, /** * Called when the momentum scroll starts (scroll which occurs as the ScrollView glides to a stop). */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index ab8ab73ffc83..f75cd1eb41bd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -17,10 +17,12 @@ import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.os.Handler; import android.view.FocusFinder; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.HorizontalScrollView; import android.widget.OverScroller; @@ -44,6 +46,8 @@ import com.facebook.react.uimanager.ViewProps; import com.facebook.react.uimanager.events.NativeGestureUtil; import com.facebook.react.views.view.ReactViewBackgroundManager; +import com.facebook.react.views.view.ReactViewGroup; +import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -51,6 +55,8 @@ /** Similar to {@link ReactScrollView} but only supports horizontal scrolling. */ public class ReactHorizontalScrollView extends HorizontalScrollView implements ReactClippingViewGroup, + ViewGroup.OnHierarchyChangeListener, + View.OnLayoutChangeListener, FabricViewStateManager.HasFabricViewStateManager, ReactOverflowView { @@ -94,11 +100,25 @@ public class ReactHorizontalScrollView extends HorizontalScrollView private @Nullable List mSnapOffsets; private boolean mSnapToStart = true; private boolean mSnapToEnd = true; + private View mContentView; private ReactViewBackgroundManager mReactBackgroundManager; private boolean mPagedArrowScrolling = false; private int pendingContentOffsetX = UNSET_CONTENT_OFFSET; private int pendingContentOffsetY = UNSET_CONTENT_OFFSET; private final FabricViewStateManager mFabricViewStateManager = new FabricViewStateManager(); + private @Nullable ReactScrollViewMaintainVisibleContentPositionData + mMaintainVisibleContentPositionData; + private @Nullable WeakReference firstVisibleViewForMaintainVisibleContentPosition = null; + private @Nullable Rect prevFirstVisibleFrameForMaintainVisibleContentPosition = null; + + private final Handler mHandler = new Handler(); + private final Runnable mComputeFirstVisibleViewRunnable = + new Runnable() { + @Override + public void run() { + computeFirstVisibleItemForMaintainVisibleContentPosition(); + } + }; private @Nullable ValueAnimator mScrollAnimator; private int mFinalAnimatedPositionScrollX = 0; @@ -136,6 +156,7 @@ public void onInitializeAccessibilityNodeInfo( }); mScroller = getOverScrollerFromParent(); + setOnHierarchyChangeListener(this); mLayoutDirection = I18nUtil.getInstance().isRTL(context) ? ViewCompat.LAYOUT_DIRECTION_RTL @@ -248,6 +269,14 @@ public void setOverflow(String overflow) { invalidate(); } + public void setMaintainVisibleContentPosition( + ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData) { + mMaintainVisibleContentPositionData = maintainVisibleContentPositionData; + if (maintainVisibleContentPositionData != null) { + computeFirstVisibleItemForMaintainVisibleContentPosition(); + } + } + @Override public @Nullable String getOverflow() { return mOverflow; @@ -436,6 +465,14 @@ protected void onScrollChanged(int x, int y, int oldX, int oldY) { mOnScrollDispatchHelper.getXFlingVelocity(), mOnScrollDispatchHelper.getYFlingVelocity()); } + + if (mMaintainVisibleContentPositionData != null) { + // We don't want to compute the first visible view everytime onScrollChanged gets called (can + // be multiple times per second). + // The following logic debounces the computation by 100ms (arbitrary value). + mHandler.removeCallbacks(mComputeFirstVisibleViewRunnable); + mHandler.postDelayed(mComputeFirstVisibleViewRunnable, 100); + } } @Override @@ -1084,6 +1121,18 @@ public void setBorderStyle(@Nullable String style) { mReactBackgroundManager.setBorderStyle(style); } + @Override + public void onChildViewAdded(View parent, View child) { + mContentView = child; + mContentView.addOnLayoutChangeListener(this); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + mContentView.removeOnLayoutChangeListener(this); + mContentView = null; + } + /** * Calls `smoothScrollTo` and updates state. * @@ -1239,6 +1288,97 @@ private void updateStateOnScroll() { updateStateOnScroll(getScrollX(), getScrollY()); } + /** + * Called when a mContentView's layout has changed. Fixes the scroll position depending on + * maintainVisibleContentPosition + */ + @Override + public void onLayoutChange( + View v, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + if (mContentView == null) { + return; + } + + if (this.mMaintainVisibleContentPositionData != null) { + scrollMaintainVisibleContentPosition(); + } + } + + /** + * Called when maintainVisibleContentPosition is used and after a scroll. Finds the first + * completely visible view in the ScrollView and stores it for later use. + */ + private void computeFirstVisibleItemForMaintainVisibleContentPosition() { + ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData = + mMaintainVisibleContentPositionData; + if (maintainVisibleContentPositionData == null) return; + + int currentScrollX = getScrollX(); + int minIdx = maintainVisibleContentPositionData.minIndexForVisible; + + ReactViewGroup contentView = (ReactViewGroup) getChildAt(0); + if (contentView == null) return; + + for (int i = minIdx; i < contentView.getChildCount(); i++) { + // 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 + View child = contentView.getChildAt(i); + if (child.getX() >= currentScrollX || i == contentView.getChildCount() - 1) { + firstVisibleViewForMaintainVisibleContentPosition = new WeakReference<>(child); + Rect frame = new Rect(); + child.getHitRect(frame); + prevFirstVisibleFrameForMaintainVisibleContentPosition = frame; + break; + } + } + } + + /** + * Called when maintainVisibleContentPosition is used and after a layout change. Detects if the + * layout change impacts the scroll position and corrects it if needed. + */ + private void scrollMaintainVisibleContentPosition() { + ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData = + this.mMaintainVisibleContentPositionData; + if (maintainVisibleContentPositionData == null) return; + + int currentScrollX = getScrollX(); + + View firstVisibleView = + firstVisibleViewForMaintainVisibleContentPosition != null + ? firstVisibleViewForMaintainVisibleContentPosition.get() + : null; + if (firstVisibleView == null) return; + Rect prevFirstVisibleFrame = this.prevFirstVisibleFrameForMaintainVisibleContentPosition; + if (prevFirstVisibleFrame == null) return; + + Rect newFrame = new Rect(); + firstVisibleView.getHitRect(newFrame); + int deltaX = newFrame.left - prevFirstVisibleFrame.left; + + if (Math.abs(deltaX) > 1) { + int scrollXTo = getScrollX() + deltaX; + + scrollTo(scrollXTo, getScrollY()); + + Integer autoScrollThreshold = maintainVisibleContentPositionData.autoScrollToTopThreshold; + if (autoScrollThreshold != null) { + // If the offset WAS within the threshold of the start, animate to the start. + if (currentScrollX - deltaX <= autoScrollThreshold) { + reactSmoothScrollTo(0, getScrollY()); + } + } + } + } + @Override public FabricViewStateManager getFabricViewStateManager() { return mFabricViewStateManager; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java index 05112f45e449..44cf48b52e15 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java @@ -316,4 +316,21 @@ public void setContentOffset(ReactHorizontalScrollView view, ReadableMap value) view.scrollTo(0, 0); } } + + @ReactProp(name = "maintainVisibleContentPosition") + public void setMaintainVisibleContentPosition(ReactHorizontalScrollView view, ReadableMap value) { + if (value != null) { + int minIndexForVisible = value.getInt("minIndexForVisible"); + Integer autoScrollToTopThreshold = + value.hasKey("autoscrollToTopThreshold") + ? value.getInt("autoscrollToTopThreshold") + : null; + ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData = + new ReactScrollViewMaintainVisibleContentPositionData( + minIndexForVisible, autoScrollToTopThreshold); + view.setMaintainVisibleContentPosition(maintainVisibleContentPositionData); + } else { + view.setMaintainVisibleContentPosition(null); + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 9fc5b76829c8..ab9b83cd9821 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -16,6 +16,7 @@ import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.os.Handler; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; @@ -43,6 +44,8 @@ import com.facebook.react.uimanager.common.ViewUtil; import com.facebook.react.uimanager.events.NativeGestureUtil; import com.facebook.react.views.view.ReactViewBackgroundManager; +import com.facebook.react.views.view.ReactViewGroup; +import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.util.List; @@ -97,6 +100,19 @@ public class ReactScrollView extends ScrollView private int pendingContentOffsetX = UNSET_CONTENT_OFFSET; private int pendingContentOffsetY = UNSET_CONTENT_OFFSET; private final FabricViewStateManager mFabricViewStateManager = new FabricViewStateManager(); + private @Nullable ReactScrollViewMaintainVisibleContentPositionData + mMaintainVisibleContentPositionData; + private @Nullable WeakReference firstVisibleViewForMaintainVisibleContentPosition = null; + private @Nullable Rect prevFirstVisibleFrameForMaintainVisibleContentPosition = null; + + private final Handler mHandler = new Handler(); + private final Runnable mComputeFirstVisibleViewRunnable = + new Runnable() { + @Override + public void run() { + computeFirstVisibleItemForMaintainVisibleContentPosition(); + } + }; private @Nullable ValueAnimator mScrollAnimator; private int mFinalAnimatedPositionScrollX; @@ -227,6 +243,14 @@ public void setOverflow(String overflow) { invalidate(); } + public void setMaintainVisibleContentPosition( + ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData) { + mMaintainVisibleContentPositionData = maintainVisibleContentPositionData; + if (maintainVisibleContentPositionData != null) { + computeFirstVisibleItemForMaintainVisibleContentPosition(); + } + } + @Override public @Nullable String getOverflow() { return mOverflow; @@ -320,6 +344,14 @@ protected void onScrollChanged(int x, int y, int oldX, int oldY) { mOnScrollDispatchHelper.getXFlingVelocity(), mOnScrollDispatchHelper.getYFlingVelocity()); } + + if (mMaintainVisibleContentPositionData != null) { + // We don't want to compute the first visible view everytime onScrollChanged gets called (can + // be multiple times per second). + // The following logic debounces the computation by 100ms (arbitrary value). + mHandler.removeCallbacks(mComputeFirstVisibleViewRunnable); + mHandler.postDelayed(mComputeFirstVisibleViewRunnable, 100); + } } @Override @@ -961,6 +993,10 @@ public void onLayoutChange( return; } + if (this.mMaintainVisibleContentPositionData != null) { + scrollMaintainVisibleContentPosition(); + } + int currentScrollY = getScrollY(); int maxScrollY = getMaxScrollY(); if (currentScrollY > maxScrollY) { @@ -968,6 +1004,73 @@ public void onLayoutChange( } } + /** + * Called when maintainVisibleContentPosition is used and after a scroll. Finds the first + * completely visible view in the ScrollView and stores it for later use. + */ + private void computeFirstVisibleItemForMaintainVisibleContentPosition() { + ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData = + mMaintainVisibleContentPositionData; + if (maintainVisibleContentPositionData == null) return; + + int currentScrollY = getScrollY(); + int minIdx = maintainVisibleContentPositionData.minIndexForVisible; + + ReactViewGroup contentView = (ReactViewGroup) mContentView; + if (contentView == null) return; + + for (int i = minIdx; i < contentView.getChildCount(); i++) { + // 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 + View child = contentView.getChildAt(i); + if (child.getY() >= currentScrollY || i == contentView.getChildCount() - 1) { + firstVisibleViewForMaintainVisibleContentPosition = new WeakReference<>(child); + Rect frame = new Rect(); + child.getHitRect(frame); + prevFirstVisibleFrameForMaintainVisibleContentPosition = frame; + break; + } + } + } + + /** + * Called when maintainVisibleContentPosition is used and after a layout change. Detects if the + * layout change impacts the scroll position and corrects it if needed. + */ + private void scrollMaintainVisibleContentPosition() { + ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData = + this.mMaintainVisibleContentPositionData; + if (maintainVisibleContentPositionData == null) return; + + int currentScrollY = getScrollY(); + + View firstVisibleView = + firstVisibleViewForMaintainVisibleContentPosition != null + ? firstVisibleViewForMaintainVisibleContentPosition.get() + : null; + if (firstVisibleView == null) return; + Rect prevFirstVisibleFrame = this.prevFirstVisibleFrameForMaintainVisibleContentPosition; + if (prevFirstVisibleFrame == null) return; + + Rect newFrame = new Rect(); + firstVisibleView.getHitRect(newFrame); + int deltaY = newFrame.top - prevFirstVisibleFrame.top; + + if (Math.abs(deltaY) > 1) { + int scrollYTo = getScrollY() + deltaY; + + scrollTo(getScrollX(), scrollYTo); + + Integer autoScrollThreshold = maintainVisibleContentPositionData.autoScrollToTopThreshold; + if (autoScrollThreshold != null) { + // If the offset WAS within the threshold of the start, animate to the start. + if (currentScrollY - deltaY <= autoScrollThreshold) { + reactSmoothScrollTo(getScrollX(), 0); + } + } + } + } + @Override public void setBackgroundColor(int color) { mReactBackgroundManager.setBackgroundColor(color); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewMaintainVisibleContentPositionData.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewMaintainVisibleContentPositionData.java new file mode 100644 index 000000000000..19d79cbe6c4a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewMaintainVisibleContentPositionData.java @@ -0,0 +1,15 @@ +package com.facebook.react.views.scroll; + +import androidx.annotation.Nullable; + +public class ReactScrollViewMaintainVisibleContentPositionData { + public final int minIndexForVisible; + + public final @Nullable Integer autoScrollToTopThreshold; + + ReactScrollViewMaintainVisibleContentPositionData( + int minIndexForVisible, @Nullable Integer autoScrollToTopThreshold) { + this.minIndexForVisible = minIndexForVisible; + this.autoScrollToTopThreshold = autoScrollToTopThreshold; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java index 974584cd8076..54c57dd825bb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java @@ -321,6 +321,23 @@ public void setContentOffset(ReactScrollView view, ReadableMap value) { } } + @ReactProp(name = "maintainVisibleContentPosition") + public void setMaintainVisibleContentPosition(ReactScrollView view, ReadableMap value) { + if (value != null) { + int minIndexForVisible = value.getInt("minIndexForVisible"); + Integer autoScrollToTopThreshold = + value.hasKey("autoscrollToTopThreshold") + ? value.getInt("autoscrollToTopThreshold") + : null; + ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData = + new ReactScrollViewMaintainVisibleContentPositionData( + minIndexForVisible, autoScrollToTopThreshold); + view.setMaintainVisibleContentPosition(maintainVisibleContentPositionData); + } else { + view.setMaintainVisibleContentPosition(null); + } + } + @Override public Object updateState( ReactScrollView view, ReactStylesDiffMap props, @Nullable StateWrapper stateWrapper) { diff --git a/packages/rn-tester/js/examples/ScrollView/ScrollViewExpandingExample.js b/packages/rn-tester/js/examples/ScrollView/ScrollViewExpandingExample.js new file mode 100644 index 000000000000..61f1b268ea2f --- /dev/null +++ b/packages/rn-tester/js/examples/ScrollView/ScrollViewExpandingExample.js @@ -0,0 +1,193 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const React = require('react'); + +const { + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + Switch, + View, +} = require('react-native'); + +const LOREM = ` +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse laoreet lorem at molestie accumsan. Mauris blandit purus sapien, ac faucibus lorem elementum a. Fusce sed odio eget arcu varius sodales vel et elit. Etiam scelerisque nunc eu aliquet cursus. Aliquam erat volutpat. Praesent fringilla tellus at neque scelerisque, sed egestas lorem lacinia. In hac habitasse platea dictumst. Mauris sed ex ut felis ultricies scelerisque a ac erat. Suspendisse sapien mauris, sodales ac mollis ac, gravida at velit. Nam dapibus a nisl in aliquam. Quisque eu velit velit. In iaculis nisi purus, non tristique erat posuere eget. Aenean bibendum massa ac turpis scelerisque ultrices. Morbi nulla erat, commodo sit amet ultrices ac, faucibus vel mi. + +Quisque turpis ex, finibus ac fermentum in, venenatis vel nisl. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec vitae blandit mi. Nullam quis dignissim ligula, sed vestibulum nibh. Fusce ligula diam, imperdiet ac est eu, consectetur laoreet risus. Sed luctus justo dui, molestie cursus quam finibus ac. Aenean nisl urna, dapibus id pellentesque vel, semper sed massa. Curabitur purus odio, facilisis in posuere eu, rutrum nec est. Sed scelerisque feugiat ante, vitae placerat nibh faucibus eget. Sed dignissim quam a turpis tincidunt bibendum egestas volutpat erat. Maecenas eleifend augue a ultricies tempus. Sed faucibus turpis id scelerisque gravida. Proin sed consequat ante, et vehicula nibh. + +Vestibulum quis fermentum purus. Nunc sed sollicitudin risus. Aliquam malesuada lorem sed nibh placerat ultricies. Nam mattis nisi sit amet tortor gravida, ut condimentum justo commodo. Curabitur ac mauris ex. Maecenas in nulla ac erat cursus ultrices eu id odio. Sed aliquet lorem nunc, vitae finibus lacus aliquam vel. Nunc et odio sit amet sem hendrerit ullamcorper nec vel odio. Sed suscipit, risus ut lobortis posuere, lacus odio ultrices orci, in luctus sem erat non odio. Mauris pretium fringilla diam a blandit. Nam ut ligula dapibus, sollicitudin magna at, malesuada leo. Vestibulum malesuada vestibulum ultricies. Curabitur varius quam ut erat venenatis pellentesque. Sed dignissim nisl eu luctus rutrum. Vestibulum vel sodales dolor, ac sagittis nunc. + +Phasellus interdum arcu quis vehicula malesuada. Curabitur bibendum neque sed tincidunt feugiat. Suspendisse id lorem nibh. Sed sit amet ipsum sapien. Donec pretium neque sem, viverra feugiat diam mollis vel. Nam arcu nisl, lacinia vel mi eget, commodo eleifend tellus. Etiam pretium non lorem quis fermentum. Vivamus ut mauris vitae odio blandit elementum at rutrum mi. Vivamus id neque accumsan, mattis erat ac, rhoncus leo. Sed tristique nibh ut maximus rutrum. Nunc turpis metus, molestie sed iaculis a, dapibus et tellus. Donec malesuada ipsum sit amet nisi vehicula volutpat. Nulla facilisi. Nam a efficitur mi. Nunc lacus diam, eleifend pellentesque tincidunt et, bibendum sit amet urna. + +Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer vestibulum, tellus vel rutrum lacinia, elit odio malesuada orci, id molestie felis arcu a ex. Proin eleifend bibendum massa, facilisis ultricies quam ultricies sit amet. Sed consequat imperdiet suscipit. Vestibulum rutrum est at sem sollicitudin, id volutpat enim semper. Etiam aliquet eleifend lorem ornare maximus. Morbi venenatis arcu id accumsan molestie. Morbi eu nisi lectus. Nam ac metus non odio placerat posuere. Aliquam consectetur facilisis magna. Nunc mollis mauris nulla, et sagittis erat tincidunt eu. + +Nulla ut varius odio. Ut laoreet sollicitudin nunc, sit amet congue quam vehicula nec. Donec eget dictum turpis. Donec sed est luctus, consequat lorem vel, auctor leo. Proin viverra ex a velit ullamcorper, quis sollicitudin purus luctus. Proin laoreet ut lorem in cursus. Cras imperdiet turpis ut nisi pulvinar consequat. Sed turpis velit, pharetra sit amet mollis vel, imperdiet vitae enim. Phasellus eget vehicula turpis. Fusce vehicula quam non justo luctus, ut bibendum sem feugiat. Ut id egestas sem. Duis efficitur sem rutrum, mattis elit id, porta lectus. Ut eu volutpat eros. Nulla tincidunt erat ac neque porttitor blandit ut quis enim. + +Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam nisi nulla, ullamcorper at urna et, aliquet porttitor turpis. Ut justo nulla, elementum mattis porttitor eget, ullamcorper mollis est. Praesent quis tortor at justo pretium aliquet. Aliquam gravida magna quis ullamcorper venenatis. Etiam ac luctus turpis. Vestibulum scelerisque sem mauris, nec lacinia leo volutpat vitae. Pellentesque ut justo est. Fusce at ante laoreet, tempus tellus sit amet, dignissim felis. Phasellus vitae quam risus. Duis condimentum aliquet diam vel consequat. Fusce ut massa finibus, elementum nisi sit amet, porta arcu. Nunc eget tortor quis ante finibus viverra auctor varius sapien. Nunc facilisis cursus risus non fringilla. Praesent malesuada, augue nec tempor elementum, massa sapien pellentesque dui, et suscipit tortor neque vel purus. + +Cras euismod at turpis quis maximus. Etiam fermentum eu orci ac auctor. Praesent egestas, purus in blandit tincidunt, nunc massa ornare tortor, a posuere mauris lacus ac nisi. Fusce vitae dictum nibh. Curabitur quis urna mauris. Phasellus et magna vitae felis elementum faucibus ut vitae erat. Quisque mollis vulputate vehicula. + +Etiam efficitur nisi sapien, non ultricies nibh condimentum id. Vivamus in lorem semper lorem congue ultrices. Aenean gravida malesuada consequat. Donec id ultrices mauris. Donec pretium eu neque ut venenatis. Integer elementum velit vel placerat dictum. Donec hendrerit dapibus nibh, a efficitur enim sollicitudin eu. Sed et turpis interdum, auctor velit ut, efficitur ex. Etiam sed pretium dui. + +Aliquam sagittis nulla ligula, quis vestibulum sapien consequat at. Maecenas non magna eu ligula interdum faucibus. Duis suscipit posuere eros volutpat hendrerit. Nullam at magna lorem. Morbi convallis sapien sit amet ipsum hendrerit ullamcorper. Phasellus in urna lacinia, feugiat augue porttitor, malesuada nunc. In ultrices aliquam arcu, non elementum nibh scelerisque at. + +Proin facilisis purus sit amet augue suscipit volutpat. Sed et purus fermentum, gravida nisi a, condimentum libero. Sed leo purus, vehicula ac porta id, mattis vitae libero. Quisque maximus a risus sit amet blandit. Donec vestibulum tortor sed lectus dignissim, in dictum massa tincidunt. Pellentesque fringilla dignissim commodo. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi sodales, est sit amet iaculis dapibus, neque massa laoreet magna, in suscipit elit diam at nulla. + +Pellentesque ac neque orci. Proin aliquet elit dictum mauris dictum convallis. Aenean gravida ac dui sit amet dapibus. In at metus in nisl bibendum semper. Donec non congue diam. Fusce sem tortor, mattis commodo felis quis, interdum commodo justo. Vivamus convallis velit vel mauris rutrum, id pretium tellus ornare. Morbi faucibus, diam at iaculis scelerisque, metus augue mattis purus, id tincidunt odio enim nec diam. Pellentesque nec lorem pharetra, vestibulum libero et, porta sapien. Nulla accumsan pharetra viverra. Vestibulum ac gravida risus, ac rutrum ante. Integer nisl libero, mollis nec cursus vitae, tincidunt quis lorem. Vestibulum efficitur nisi vel mauris venenatis bibendum. Curabitur elementum, tellus at placerat dictum, neque felis condimentum elit, ac efficitur velit urna eu nisl. Maecenas sed nisi risus. + +Quisque pharetra libero ut nisi ultricies, nec tempus mauris rhoncus. Proin eget sem libero. Nullam lacinia nisl sed nulla dapibus pulvinar. Ut varius iaculis turpis, non varius magna consequat quis. Integer eu nisl erat. Praesent pulvinar fringilla porttitor. Vestibulum fringilla laoreet augue, et elementum massa cursus quis. Suspendisse eros purus, eleifend in magna sit amet, gravida porttitor ligula. Sed id mattis justo. Aenean accumsan, dolor eu auctor cursus, lacus nulla ultricies ex, aliquam varius dui quam ac enim. Donec non consequat mauris, vel elementum augue. Vestibulum magna metus, euismod sollicitudin sem et, commodo consectetur dui. Aliquam dapibus tristique dolor nec ultricies. Morbi id nunc finibus, consequat ante eu, blandit justo. Nullam volutpat finibus felis, non semper nunc faucibus eget. Curabitur luctus, quam a egestas ullamcorper, nibh dui pulvinar tortor, sed aliquam turpis lacus nec orci. + +Suspendisse et tempor turpis, sed pulvinar metus. Fusce condimentum, felis non eleifend ullamcorper, enim mi dapibus orci, eu gravida felis est in justo. Nunc in pulvinar nulla, at mattis enim. Pellentesque pellentesque posuere velit sit amet laoreet. Aliquam erat volutpat. Mauris dapibus odio ac enim consequat, a facilisis tortor fringilla. Vivamus blandit, neque a faucibus placerat, est orci suscipit quam, ut convallis nibh velit sit amet sem. Integer quis iaculis erat. + +Mauris mattis tempus purus, quis venenatis erat imperdiet in. Donec est odio, tristique vel pharetra eget, sagittis at eros. Donec aliquam ultrices ligula, convallis malesuada sapien feugiat tincidunt. Aliquam iaculis eros nec lacus malesuada euismod. Duis euismod urna rhoncus, tincidunt nisl et, pretium metus. Integer nec sem odio. Cras mattis ullamcorper libero vitae facilisis. Nullam a blandit elit, a congue nisi. Etiam interdum maximus interdum. Vivamus bibendum tellus sodales tellus efficitur, at gravida urna placerat. Phasellus eget tempus orci. Donec eget risus dignissim, rutrum nulla nec, ultricies tellus. Vivamus vitae commodo arcu. + +Aliquam erat volutpat. Integer porttitor tortor feugiat imperdiet luctus. Curabitur et viverra justo. Suspendisse vehicula mauris eget augue tempor, in tempor libero tristique. Sed quis justo scelerisque, faucibus tortor vel, eleifend libero. Nulla ultricies fermentum mauris, a tempor mauris consectetur eu. Phasellus felis odio, mollis eu semper viverra, scelerisque nec tortor. Cras in metus sed est tincidunt faucibus a vel lectus. Aenean placerat sit amet orci vel elementum. Nunc auctor nisi ullamcorper malesuada egestas. Phasellus pulvinar facilisis aliquet. + +Sed vel libero maximus, ullamcorper nulla a, venenatis ligula. Donec at egestas ipsum, vel commodo urna. Maecenas sit amet sapien odio. Vivamus tempus sem quis massa dignissim vehicula. Aliquam nec sapien eget lectus elementum ornare. Integer imperdiet eu dolor aliquam tincidunt. Proin velit arcu, imperdiet nec finibus quis, fermentum id odio. Sed eget velit ipsum. Nunc sed pretium nisl. Vivamus nibh purus, interdum in metus eget, tristique porttitor sapien. Vivamus laoreet rhoncus odio, ac feugiat velit hendrerit id. Ut vitae faucibus augue, volutpat lobortis urna. Sed molestie nisi non lorem molestie bibendum. + +Mauris vulputate metus venenatis, finibus augue vel, elementum neque. Donec laoreet, nunc sit amet elementum facilisis, ex mi porta erat, non venenatis nibh lectus eu nulla. Etiam euismod, dolor quis vulputate pulvinar, mi magna pretium tellus, eu interdum est ante eget leo. Aenean egestas mi viverra accumsan pharetra. Nullam aliquam eros consectetur ornare varius. Nam porttitor lacus dolor, a lobortis mauris porta ultrices. Suspendisse potenti. + +Fusce pretium, libero ac dapibus sollicitudin, enim arcu interdum libero, suscipit euismod velit lectus tempor arcu. Sed venenatis lorem et nulla pretium, et ullamcorper sapien malesuada. Mauris ut dictum orci. Morbi eu lectus faucibus, rutrum dui nec, scelerisque ex. Donec vitae erat metus. Aliquam turpis diam, sodales nec commodo pretium, ornare nec metus. Proin elementum, est efficitur malesuada viverra, odio dui luctus orci, a elementum tortor enim quis arcu. Nullam molestie libero ac accumsan auctor. Aliquam consequat odio pretium orci ultricies, a ullamcorper tellus bibendum. Etiam tortor quam, fringilla nec lacus et, tincidunt ultrices mauris. Morbi lacus felis, pellentesque aliquam nisl ut, imperdiet congue lectus. Aliquam non scelerisque enim, vitae tincidunt nulla. Integer odio nisl, viverra sit amet malesuada nec, consectetur consequat turpis. + +Maecenas quis turpis vehicula, efficitur lorem eu, tempor lectus. Suspendisse porttitor ex vel nunc imperdiet iaculis. Maecenas dictum vel nisl ac ultricies. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam ac lectus porttitor, posuere nisi sed, ultricies est. Curabitur ullamcorper, dui ut vestibulum rhoncus, nibh velit pulvinar eros, vel bibendum felis quam id nibh. Integer ut purus cursus quam molestie pulvinar. Phasellus dictum nisi eu augue molestie commodo. +`; + +type Props = $ReadOnly<{||}>; +type State = { + count: number, + horizontal: boolean, + isExpanded: {[key: number]: boolean}, + maintainVisibleContentPosition?: {minIndexForVisible: number} | null, +}; + +class ScrollViewExpandingExample extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + count: 20, + isExpanded: {}, + horizontal: false, + maintainVisibleContentPosition: null, + }; + } + + makeItems: (nItems: number, styles: any) => Array = ( + nItems: number, + styles, + ): Array => { + const items = []; + for (let i = 0; i < nItems; i++) { + const onPress = () => { + this.state.isExpanded[i] = !this.state.isExpanded[i]; + this.setState(this.state); + }; + + const onLongPress = () => { + this.state.count = this.state.count + 100; + this.setState(this.state); + }; + + items[i] = ( + + {this.state.isExpanded[i] ? LOREM : `Item ${i}`} + + ); + } + return items; + }; + + render(): React.Node { + // One of the items is a horizontal scroll view + const items = this.makeItems(this.state.count, styles.itemWrapper); + + const onMaintainVisibleContentPositionSwitchChange = () => { + this.state.maintainVisibleContentPosition = this.state + .maintainVisibleContentPosition + ? null + : {minIndexForVisible: 0}; + this.setState(this.state); + }; + + const onHorizontalSwitchChange = () => { + this.state.horizontal = !this.state.horizontal; + this.setState(this.state); + }; + + return ( + <> + + Maintain visible content position (index: 0) + + + + Horizontal ScrollView? + + + + {items} + + + ); + } +} + +const styles = StyleSheet.create({ + verticalScrollView: { + margin: 10, + }, + itemWrapper: { + backgroundColor: '#dddddd', + alignItems: 'center', + borderRadius: 5, + borderWidth: 5, + borderColor: '#a52a2a', + padding: 30, + margin: 5, + }, + horizontalItemWrapper: { + padding: 50, + }, + horizontalPagingItemWrapper: { + width: 200, + }, +}); + +exports.title = ''; +exports.description = + 'Component that enables keeps scroll position through layout changes.'; +exports.simpleExampleContainer = true; +exports.examples = [ + { + title: 'Expandable scroll view', + render: function(): React.Element { + return ; + }, + }, +]; diff --git a/packages/rn-tester/js/utils/RNTesterList.android.js b/packages/rn-tester/js/utils/RNTesterList.android.js index f528ca65eb43..85c5872b84ce 100644 --- a/packages/rn-tester/js/utils/RNTesterList.android.js +++ b/packages/rn-tester/js/utils/RNTesterList.android.js @@ -94,6 +94,10 @@ const Components: Array = [ category: 'Basic', module: require('../examples/ScrollView/ScrollViewAnimatedExample'), }, + { + key: 'ScrollViewExpandingExample', + module: require('../examples/ScrollView/ScrollViewExpandingExample'), + }, { key: 'SectionListExample', category: 'ListView', diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index 6eec793053fb..80fc603717ea 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -118,6 +118,10 @@ const Components: Array = [ module: require('../examples/ScrollView/ScrollViewAnimatedExample'), supportsTVOS: true, }, + { + key: 'ScrollViewExpandingExample', + module: require('../examples/ScrollView/ScrollViewExpandingExample'), + }, { key: 'ScrollViewIndicatorInsetsExample', module: require('../examples/ScrollView/ScrollViewIndicatorInsetsExample'),