Skip to content

Commit ec1e973

Browse files
rshestfacebook-github-bot
authored andcommitted
View recycling for ScrollView native component on Android (facebook#53395)
Summary: Pull Request resolved: facebook#53395 # Changelog [Internal] - This enables view recycling for both ScrollView and HorizontalScrollView on Android. The feature is gated by the corresponding RN feature flag, `enableViewRecyclingForScrollView` (which is false by default for now, will be enabled in an experiment). Reviewed By: lenaic Differential Revision: D80611087 fbshipit-source-id: b3026affc0ea61bc7739126d6529c83f2a653183
1 parent cf52852 commit ec1e973

File tree

9 files changed

+201
-52
lines changed

9 files changed

+201
-52
lines changed

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5647,6 +5647,8 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollViewManager :
56475647
public fun flashScrollIndicators (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;)V
56485648
public synthetic fun flashScrollIndicators (Ljava/lang/Object;)V
56495649
public fun getName ()Ljava/lang/String;
5650+
public synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View;
5651+
protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;)Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;
56505652
public synthetic fun receiveCommand (Landroid/view/View;ILcom/facebook/react/bridge/ReadableArray;)V
56515653
public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
56525654
public fun receiveCommand (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;ILcom/facebook/react/bridge/ReadableArray;)V
@@ -5910,6 +5912,8 @@ public class com/facebook/react/views/scroll/ReactScrollViewManager : com/facebo
59105912
public fun getCommandsMap ()Ljava/util/Map;
59115913
public fun getExportedCustomDirectEventTypeConstants ()Ljava/util/Map;
59125914
public fun getName ()Ljava/lang/String;
5915+
public synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View;
5916+
protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/scroll/ReactScrollView;)Lcom/facebook/react/views/scroll/ReactScrollView;
59135917
public synthetic fun receiveCommand (Landroid/view/View;ILcom/facebook/react/bridge/ReadableArray;)V
59145918
public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
59155919
public fun receiveCommand (Lcom/facebook/react/views/scroll/ReactScrollView;ILcom/facebook/react/bridge/ReadableArray;)V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<19022bb7e93ed79a1cbe86c71f34bd50>>
7+
* @generated SignedSource<<3c47ba9c7fdbb37f0e06fdca56de3223>>
88
*/
99

1010
/**
@@ -99,7 +99,7 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
9999

100100
override fun enableViewRecycling(): Boolean = false
101101

102-
override fun enableViewRecyclingForScrollView(): Boolean = true
102+
override fun enableViewRecyclingForScrollView(): Boolean = false
103103

104104
override fun enableViewRecyclingForText(): Boolean = true
105105

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
9696
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
9797
private final @Nullable OverScroller mScroller;
9898
private final VelocityHelper mVelocityHelper = new VelocityHelper();
99-
private final Rect mOverflowInset = new Rect();
99+
private final Rect mTempRect = new Rect();
100+
private final ValueAnimator DEFAULT_FLING_ANIMATOR = ObjectAnimator.ofInt(this, "scrollX", 0, 0);
100101

102+
private Rect mOverflowInset = new Rect();
101103
private boolean mActivelyScrolling;
102104
private @Nullable Rect mClippingRect;
103105
private Overflow mOverflow = Overflow.SCROLL;
@@ -118,11 +120,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
118120
private boolean mSnapToEnd = true;
119121
private int mSnapToAlignment = SNAP_ALIGNMENT_DISABLED;
120122
private boolean mPagedArrowScrolling = false;
121-
private int pendingContentOffsetX = UNSET_CONTENT_OFFSET;
122-
private int pendingContentOffsetY = UNSET_CONTENT_OFFSET;
123+
private int mPendingContentOffsetX = UNSET_CONTENT_OFFSET;
124+
private int mPendingContentOffsetY = UNSET_CONTENT_OFFSET;
123125
private @Nullable StateWrapper mStateWrapper = null;
124-
private final ReactScrollViewScrollState mReactScrollViewScrollState;
125-
private final ValueAnimator DEFAULT_FLING_ANIMATOR = ObjectAnimator.ofInt(this, "scrollX", 0, 0);
126+
private ReactScrollViewScrollState mReactScrollViewScrollState;
126127
private PointerEvents mPointerEvents = PointerEvents.AUTO;
127128
private long mLastScrollDispatchTime = 0;
128129
private int mScrollEventThrottle = 0;
@@ -131,8 +132,6 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
131132
private int mFadingEdgeLengthStart = 0;
132133
private int mFadingEdgeLengthEnd = 0;
133134

134-
private final Rect mTempRect = new Rect();
135-
136135
public ReactHorizontalScrollView(Context context) {
137136
this(context, null);
138137
}
@@ -144,12 +143,67 @@ public ReactHorizontalScrollView(Context context, @Nullable FpsListener fpsListe
144143
ViewCompat.setAccessibilityDelegate(this, new ReactScrollViewAccessibilityDelegate());
145144

146145
mScroller = getOverScrollerFromParent();
147-
mReactScrollViewScrollState = new ReactScrollViewScrollState();
148146

149147
setOnHierarchyChangeListener(this);
150148
setClipChildren(false);
149+
initView();
151150
}
152151

152+
/**
153+
* Set all default values here as opposed to in the constructor or field defaults. It is important
154+
* that these properties are set during the constructor, but also on-demand whenever an existing
155+
* ReactTextView is recycled.
156+
*/
157+
private void initView() {
158+
mOverflowInset = new Rect();
159+
mActivelyScrolling = false;
160+
mClippingRect = null;
161+
mOverflow = Overflow.SCROLL;
162+
mDragging = false;
163+
mPagingEnabled = false;
164+
mPostTouchRunnable = null;
165+
mRemoveClippedSubviews = false;
166+
mScrollEnabled = true;
167+
mSendMomentumEvents = false;
168+
mFpsListener = null;
169+
mScrollPerfTag = null;
170+
mEndBackground = null;
171+
mEndFillColor = Color.TRANSPARENT;
172+
mDisableIntervalMomentum = false;
173+
mSnapInterval = 0;
174+
mSnapOffsets = null;
175+
mSnapToStart = true;
176+
mSnapToEnd = true;
177+
mSnapToAlignment = SNAP_ALIGNMENT_DISABLED;
178+
mPagedArrowScrolling = false;
179+
mPendingContentOffsetX = UNSET_CONTENT_OFFSET;
180+
mPendingContentOffsetY = UNSET_CONTENT_OFFSET;
181+
mStateWrapper = null;
182+
mReactScrollViewScrollState = new ReactScrollViewScrollState();
183+
184+
mPointerEvents = PointerEvents.AUTO;
185+
mLastScrollDispatchTime = 0;
186+
mScrollEventThrottle = 0;
187+
mContentView = null;
188+
mMaintainVisibleContentPositionHelper = null;
189+
mFadingEdgeLengthStart = 0;
190+
mFadingEdgeLengthEnd = 0;
191+
}
192+
193+
/* package */ void recycleView() {
194+
// Set default field values
195+
initView();
196+
197+
// If the view is still attached to a parent, we need to remove it from the parent
198+
// before we can recycle it.
199+
if (getParent() != null) {
200+
((ViewGroup) getParent()).removeView(this);
201+
}
202+
updateView();
203+
}
204+
205+
private void updateView() {}
206+
153207
@Override
154208
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
155209
super.onInitializeAccessibilityNodeInfo(info);
@@ -441,9 +495,9 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) {
441495
// If a "pending" content offset value has been set, we restore that value.
442496
// Upon call to scrollTo, the "pending" values will be re-set.
443497
int scrollToX =
444-
pendingContentOffsetX != UNSET_CONTENT_OFFSET ? pendingContentOffsetX : getScrollX();
498+
mPendingContentOffsetX != UNSET_CONTENT_OFFSET ? mPendingContentOffsetX : getScrollX();
445499
int scrollToY =
446-
pendingContentOffsetY != UNSET_CONTENT_OFFSET ? pendingContentOffsetY : getScrollY();
500+
mPendingContentOffsetY != UNSET_CONTENT_OFFSET ? mPendingContentOffsetY : getScrollY();
447501
scrollTo(scrollToX, scrollToY);
448502
}
449503

@@ -1459,11 +1513,11 @@ private void setPendingContentOffsets(int x, int y) {
14591513
}
14601514

14611515
if (isContentReady()) {
1462-
pendingContentOffsetX = UNSET_CONTENT_OFFSET;
1463-
pendingContentOffsetY = UNSET_CONTENT_OFFSET;
1516+
mPendingContentOffsetX = UNSET_CONTENT_OFFSET;
1517+
mPendingContentOffsetY = UNSET_CONTENT_OFFSET;
14641518
} else {
1465-
pendingContentOffsetX = x;
1466-
pendingContentOffsetY = y;
1519+
mPendingContentOffsetX = x;
1520+
mPendingContentOffsetY = y;
14671521
}
14681522
}
14691523

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.facebook.react.bridge.ReadableArray
1414
import com.facebook.react.bridge.ReadableMap
1515
import com.facebook.react.bridge.ReadableType
1616
import com.facebook.react.bridge.RetryableMountingLayerException
17+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
1718
import com.facebook.react.module.annotations.ReactModule
1819
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderColor
1920
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderRadius
@@ -55,6 +56,23 @@ public open class ReactHorizontalScrollViewManager
5556
@JvmOverloads
5657
constructor(private val fpsListener: FpsListener? = null) :
5758
ViewGroupManager<ReactHorizontalScrollView>(), ScrollCommandHandler<ReactHorizontalScrollView> {
59+
init {
60+
if (ReactNativeFeatureFlags.enableViewRecyclingForScrollView()) {
61+
setupViewRecycling()
62+
}
63+
}
64+
65+
override fun prepareToRecycleView(
66+
reactContext: ThemedReactContext,
67+
view: ReactHorizontalScrollView,
68+
): ReactHorizontalScrollView? {
69+
// BaseViewManager
70+
val preparedView = super.prepareToRecycleView(reactContext, view)
71+
if (preparedView != null) {
72+
preparedView.recycleView()
73+
}
74+
return preparedView
75+
}
5876

5977
override fun getName(): String = REACT_CLASS
6078

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -96,43 +96,41 @@ public class ReactScrollView extends ScrollView
9696
private final @Nullable OverScroller mScroller;
9797
private final VelocityHelper mVelocityHelper = new VelocityHelper();
9898
private final Rect mTempRect = new Rect();
99-
private final Rect mOverflowInset = new Rect();
99+
private final ValueAnimator DEFAULT_FLING_ANIMATOR = ObjectAnimator.ofInt(this, "scrollY", 0, 0);
100100

101+
private Rect mOverflowInset;
101102
private @Nullable VirtualViewContainerState mVirtualViewContainerState;
102103
private boolean mActivelyScrolling;
103104
private @Nullable Rect mClippingRect;
104-
private Overflow mOverflow = Overflow.SCROLL;
105+
private Overflow mOverflow;
105106
private boolean mDragging;
106-
private boolean mPagingEnabled = false;
107+
private boolean mPagingEnabled;
107108
private @Nullable Runnable mPostTouchRunnable;
108109
private boolean mRemoveClippedSubviews;
109-
private boolean mScrollEnabled = true;
110+
private boolean mScrollEnabled;
110111
private boolean mSendMomentumEvents;
111-
private @Nullable FpsListener mFpsListener = null;
112+
private @Nullable FpsListener mFpsListener;
112113
private @Nullable String mScrollPerfTag;
113114
private @Nullable Drawable mEndBackground;
114-
private int mEndFillColor = Color.TRANSPARENT;
115-
private boolean mDisableIntervalMomentum = false;
116-
private int mSnapInterval = 0;
115+
private int mEndFillColor;
116+
private boolean mDisableIntervalMomentum;
117+
private int mSnapInterval;
117118
private @Nullable List<Integer> mSnapOffsets;
118-
private boolean mSnapToStart = true;
119-
private boolean mSnapToEnd = true;
120-
private int mSnapToAlignment = SNAP_ALIGNMENT_DISABLED;
119+
private boolean mSnapToStart;
120+
private boolean mSnapToEnd;
121+
private int mSnapToAlignment;
121122
private @Nullable View mContentView;
122-
private @Nullable ReadableMap mCurrentContentOffset = null;
123-
private int pendingContentOffsetX = UNSET_CONTENT_OFFSET;
124-
private int pendingContentOffsetY = UNSET_CONTENT_OFFSET;
125-
private @Nullable StateWrapper mStateWrapper = null;
126-
private final ReactScrollViewScrollState mReactScrollViewScrollState =
127-
new ReactScrollViewScrollState();
128-
private final ValueAnimator DEFAULT_FLING_ANIMATOR = ObjectAnimator.ofInt(this, "scrollY", 0, 0);
129-
private PointerEvents mPointerEvents = PointerEvents.AUTO;
130-
private long mLastScrollDispatchTime = 0;
131-
private int mScrollEventThrottle = 0;
132-
private @Nullable MaintainVisibleScrollPositionHelper mMaintainVisibleContentPositionHelper =
133-
null;
134-
private int mFadingEdgeLengthStart = 0;
135-
private int mFadingEdgeLengthEnd = 0;
123+
private @Nullable ReadableMap mCurrentContentOffset;
124+
private int mPendingContentOffsetX;
125+
private int mPendingContentOffsetY;
126+
private @Nullable StateWrapper mStateWrapper;
127+
private ReactScrollViewScrollState mReactScrollViewScrollState;
128+
private PointerEvents mPointerEvents;
129+
private long mLastScrollDispatchTime;
130+
private int mScrollEventThrottle;
131+
private @Nullable MaintainVisibleScrollPositionHelper mMaintainVisibleContentPositionHelper;
132+
private int mFadingEdgeLengthStart;
133+
private int mFadingEdgeLengthEnd;
136134

137135
public ReactScrollView(Context context) {
138136
this(context, null);
@@ -148,8 +146,64 @@ public ReactScrollView(Context context, @Nullable FpsListener fpsListener) {
148146
setClipChildren(false);
149147

150148
ViewCompat.setAccessibilityDelegate(this, new ReactScrollViewAccessibilityDelegate());
149+
initView();
151150
}
152151

152+
/**
153+
* Set all default values here as opposed to in the constructor or field defaults. It is important
154+
* that these properties are set during the constructor, but also on-demand whenever an existing
155+
* ReactTextView is recycled.
156+
*/
157+
private void initView() {
158+
mOverflowInset = new Rect();
159+
mVirtualViewContainerState = null;
160+
mActivelyScrolling = false;
161+
mClippingRect = null;
162+
mOverflow = Overflow.SCROLL;
163+
mDragging = false;
164+
mPagingEnabled = false;
165+
mPostTouchRunnable = null;
166+
mRemoveClippedSubviews = false;
167+
mScrollEnabled = true;
168+
mSendMomentumEvents = false;
169+
mFpsListener = null;
170+
mScrollPerfTag = null;
171+
mEndBackground = null;
172+
mEndFillColor = Color.TRANSPARENT;
173+
mDisableIntervalMomentum = false;
174+
mSnapInterval = 0;
175+
mSnapOffsets = null;
176+
mSnapToStart = true;
177+
mSnapToEnd = true;
178+
mSnapToAlignment = SNAP_ALIGNMENT_DISABLED;
179+
mContentView = null;
180+
mCurrentContentOffset = null;
181+
mPendingContentOffsetX = UNSET_CONTENT_OFFSET;
182+
mPendingContentOffsetY = UNSET_CONTENT_OFFSET;
183+
mStateWrapper = null;
184+
mReactScrollViewScrollState = new ReactScrollViewScrollState();
185+
mPointerEvents = PointerEvents.AUTO;
186+
mLastScrollDispatchTime = 0;
187+
mScrollEventThrottle = 0;
188+
mMaintainVisibleContentPositionHelper = null;
189+
mFadingEdgeLengthStart = 0;
190+
mFadingEdgeLengthEnd = 0;
191+
}
192+
193+
/* package */ void recycleView() {
194+
// Set default field values
195+
initView();
196+
197+
// If the view is still attached to a parent, we need to remove it from the parent
198+
// before we can recycle it.
199+
if (getParent() != null) {
200+
((ViewGroup) getParent()).removeView(this);
201+
}
202+
updateView();
203+
}
204+
205+
private void updateView() {}
206+
153207
@Override
154208
public VirtualViewContainerState getVirtualViewContainerState() {
155209
if (mVirtualViewContainerState == null) {
@@ -368,9 +422,9 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) {
368422
// If a "pending" content offset value has been set, we restore that value.
369423
// Upon call to scrollTo, the "pending" values will be re-set.
370424
int scrollToX =
371-
pendingContentOffsetX != UNSET_CONTENT_OFFSET ? pendingContentOffsetX : getScrollX();
425+
mPendingContentOffsetX != UNSET_CONTENT_OFFSET ? mPendingContentOffsetX : getScrollX();
372426
int scrollToY =
373-
pendingContentOffsetY != UNSET_CONTENT_OFFSET ? pendingContentOffsetY : getScrollY();
427+
mPendingContentOffsetY != UNSET_CONTENT_OFFSET ? mPendingContentOffsetY : getScrollY();
374428
scrollTo(scrollToX, scrollToY);
375429
}
376430

@@ -1269,11 +1323,11 @@ private boolean isContentReady() {
12691323
*/
12701324
private void setPendingContentOffsets(int x, int y) {
12711325
if (isContentReady()) {
1272-
pendingContentOffsetX = UNSET_CONTENT_OFFSET;
1273-
pendingContentOffsetY = UNSET_CONTENT_OFFSET;
1326+
mPendingContentOffsetX = UNSET_CONTENT_OFFSET;
1327+
mPendingContentOffsetY = UNSET_CONTENT_OFFSET;
12741328
} else {
1275-
pendingContentOffsetX = x;
1276-
pendingContentOffsetY = y;
1329+
mPendingContentOffsetX = x;
1330+
mPendingContentOffsetY = y;
12771331
}
12781332
}
12791333

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.facebook.react.bridge.ReadableArray
1515
import com.facebook.react.bridge.ReadableMap
1616
import com.facebook.react.bridge.ReadableType
1717
import com.facebook.react.bridge.RetryableMountingLayerException
18+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
1819
import com.facebook.react.module.annotations.ReactModule
1920
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderColor
2021
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderRadius
@@ -56,6 +57,24 @@ public open class ReactScrollViewManager
5657
constructor(private val fpsListener: FpsListener? = null) :
5758
ViewGroupManager<ReactScrollView>(), ScrollCommandHandler<ReactScrollView> {
5859

60+
init {
61+
if (ReactNativeFeatureFlags.enableViewRecyclingForScrollView()) {
62+
setupViewRecycling()
63+
}
64+
}
65+
66+
override fun prepareToRecycleView(
67+
reactContext: ThemedReactContext,
68+
view: ReactScrollView,
69+
): ReactScrollView? {
70+
// BaseViewManager
71+
val preparedView = super.prepareToRecycleView(reactContext, view)
72+
if (preparedView != null) {
73+
preparedView.recycleView()
74+
}
75+
return preparedView
76+
}
77+
5978
override fun getName(): String = REACT_CLASS
6079

6180
public override fun createViewInstance(context: ThemedReactContext): ReactScrollView =

0 commit comments

Comments
 (0)