Skip to content

Commit 565c15f

Browse files
CalixTangmeta-codesync[bot]
authored andcommitted
Split VirtualViewContainerState into classic, experimental versions [Android] (facebook#54162)
Summary: Pull Request resolved: facebook#54162 Changelog [Internal]: Splits VirtualViewContainerState implementation into three parts: (1) an abstract parent class, (2) the "classic" version (existing version), and (3) the "experimental" version (new changes to be made). The experimental version is a copy of the classic version for now. Reviewed By: lunaleaps Differential Revision: D84569206
1 parent 9319c70 commit 565c15f

File tree

5 files changed

+219
-87
lines changed

5 files changed

+219
-87
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ private void updateView() {}
216216
@Override
217217
public VirtualViewContainerState getVirtualViewContainerState() {
218218
if (mVirtualViewContainerState == null) {
219-
mVirtualViewContainerState = new VirtualViewContainerState(this);
219+
mVirtualViewContainerState = VirtualViewContainerState.create(this);
220220
}
221221

222222
return mVirtualViewContainerState;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ private void updateView() {}
213213
@Override
214214
public VirtualViewContainerState getVirtualViewContainerState() {
215215
if (mVirtualViewContainerState == null) {
216-
mVirtualViewContainerState = new VirtualViewContainerState(this);
216+
mVirtualViewContainerState = VirtualViewContainerState.create(this);
217217
}
218218

219219
return mVirtualViewContainerState;

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

Lines changed: 28 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ internal interface VirtualViewContainer {
2222

2323
public interface VirtualView {
2424
public val virtualViewID: String
25-
2625
public val containerRelativeRect: Rect
2726

2827
public fun onModeChange(newMode: VirtualViewMode, thresholdRect: Rect): Unit
@@ -35,7 +34,7 @@ public interface VirtualView {
3534
* considered to overlap with another Rect if the line or point is within the rect bounds. However,
3635
* two Rects are not considered to overlap if they only share a boundary.
3736
*/
38-
private fun rectsOverlap(rect1: Rect, rect2: Rect): Boolean {
37+
internal fun rectsOverlap(rect1: Rect, rect2: Rect): Boolean {
3938
if (rect1.top >= rect2.bottom || rect2.top >= rect1.bottom) {
4039
// No overlap on the y-axis.
4140
return false
@@ -47,17 +46,15 @@ private fun rectsOverlap(rect1: Rect, rect2: Rect): Boolean {
4746
return true
4847
}
4948

50-
internal class VirtualViewContainerState {
51-
52-
private val prerenderRatio: Double = ReactNativeFeatureFlags.virtualViewPrerenderRatio()
53-
private val hysteresisRatio: Double = ReactNativeFeatureFlags.virtualViewHysteresisRatio()
54-
55-
private val virtualViews: MutableSet<VirtualView> = mutableSetOf()
56-
private val emptyRect: Rect = Rect()
57-
private val visibleRect: Rect = Rect()
58-
private val prerenderRect: Rect = Rect()
59-
private val hysteresisRect: Rect = Rect()
60-
private val onWindowFocusChangeListener =
49+
internal abstract class VirtualViewContainerState {
50+
protected val prerenderRatio: Double = ReactNativeFeatureFlags.virtualViewPrerenderRatio()
51+
protected val hysteresisRatio: Double = ReactNativeFeatureFlags.virtualViewHysteresisRatio()
52+
protected abstract val virtualViews: MutableCollection<VirtualView>
53+
protected val emptyRect: Rect = Rect()
54+
protected val visibleRect: Rect = Rect()
55+
protected val prerenderRect: Rect = Rect()
56+
protected val hysteresisRect: Rect = Rect()
57+
protected val onWindowFocusChangeListener =
6158
if (ReactNativeFeatureFlags.enableVirtualViewWindowFocusDetection()) {
6259
ViewTreeObserver.OnWindowFocusChangeListener {
6360
debugLog("onWindowFocusChanged")
@@ -66,8 +63,18 @@ internal class VirtualViewContainerState {
6663
} else {
6764
null
6865
}
66+
protected val scrollView: ViewGroup
6967

70-
private val scrollView: ViewGroup
68+
companion object {
69+
@JvmStatic
70+
fun create(scrollView: ViewGroup): VirtualViewContainerState {
71+
return if (ReactNativeFeatureFlags.enableVirtualViewContainerStateExperimental()) {
72+
VirtualViewContainerStateExperimental(scrollView)
73+
} else {
74+
VirtualViewContainerStateClassic(scrollView)
75+
}
76+
}
77+
}
7178

7279
constructor(scrollView: ViewGroup) {
7380
this.scrollView = scrollView
@@ -76,13 +83,13 @@ internal class VirtualViewContainerState {
7683
}
7784
}
7885

79-
public fun cleanup() {
86+
fun cleanup() {
8087
if (onWindowFocusChangeListener != null) {
8188
scrollView.viewTreeObserver.removeOnWindowFocusChangeListener(onWindowFocusChangeListener)
8289
}
8390
}
8491

85-
public fun onChange(virtualView: VirtualView) {
92+
open fun onChange(virtualView: VirtualView) {
8693
if (virtualViews.add(virtualView)) {
8794
debugLog("add", { "virtualViewID=${virtualView.virtualViewID}" })
8895
} else {
@@ -91,91 +98,27 @@ internal class VirtualViewContainerState {
9198
updateModes(virtualView)
9299
}
93100

94-
public fun remove(virtualView: VirtualView) {
101+
open fun remove(virtualView: VirtualView) {
95102
assert(virtualViews.remove(virtualView)) {
96103
"Attempting to remove non-existent VirtualView: ${virtualView.virtualViewID}"
97104
}
98105
debugLog("remove", { "virtualViewID=${virtualView.virtualViewID}" })
99106
}
100107

101108
// Called on ScrollView onLayout or onScroll
102-
public fun updateState() {
109+
fun updateState() {
103110
debugLog("updateState")
104111
updateModes()
105112
}
106113

107-
private fun updateModes(virtualView: VirtualView? = null) {
108-
scrollView.getDrawingRect(visibleRect)
109-
110-
// This happens because ScrollView content isn't ready yet. The danger here is if ScrollView
111-
// intentionally goes but curently ScrollView and v1 Fling use this check to determine if
112-
// "content ready"
113-
if (visibleRect.isEmpty()) {
114-
debugLog("updateModes", { "scrollView visibleRect is empty" })
115-
return
116-
}
117-
118-
prerenderRect.set(visibleRect)
119-
prerenderRect.inset(
120-
(-prerenderRect.width() * prerenderRatio).toInt(),
121-
(-prerenderRect.height() * prerenderRatio).toInt(),
122-
)
123-
124-
if (hysteresisRatio > 0.0) {
125-
hysteresisRect.set(prerenderRect)
126-
hysteresisRect.inset(
127-
(-visibleRect.width() * hysteresisRatio).toInt(),
128-
(-visibleRect.height() * hysteresisRatio).toInt(),
129-
)
130-
}
131-
132-
val virtualViewsIt =
133-
if (virtualView != null) listOf(virtualView) else virtualViews.toMutableSet()
134-
virtualViewsIt.forEach { vv ->
135-
val rect = vv.containerRelativeRect
136-
137-
var mode: VirtualViewMode? = VirtualViewMode.Hidden
138-
var thresholdRect = emptyRect
139-
when {
140-
rectsOverlap(rect, visibleRect) -> {
141-
thresholdRect = visibleRect
142-
if (onWindowFocusChangeListener != null) {
143-
if (scrollView.hasWindowFocus()) {
144-
mode = VirtualViewMode.Visible
145-
} else {
146-
mode = VirtualViewMode.Prerender
147-
}
148-
} else {
149-
mode = VirtualViewMode.Visible
150-
}
151-
}
152-
rectsOverlap(rect, prerenderRect) -> {
153-
mode = VirtualViewMode.Prerender
154-
thresholdRect = prerenderRect
155-
}
156-
(hysteresisRatio > 0.0 && rectsOverlap(rect, hysteresisRect)) -> {
157-
mode = null
158-
}
159-
}
160-
161-
if (mode != null) {
162-
vv.onModeChange(mode, thresholdRect)
163-
debugLog(
164-
"updateModes",
165-
{
166-
"virtualView=${vv.virtualViewID} mode=$mode rect=$rect thresholdRect=$thresholdRect"
167-
},
168-
)
169-
}
170-
}
171-
}
114+
protected abstract fun updateModes(virtualView: VirtualView? = null)
172115
}
173116

174117
private const val DEBUG_TAG: String = "VirtualViewContainerState"
175-
private val IS_DEBUG_BUILD =
118+
internal val IS_DEBUG_BUILD =
176119
ReactBuildConfig.DEBUG || ReactBuildConfig.IS_INTERNAL_BUILD || ReactBuildConfig.ENABLE_PERFETTO
177120

178-
internal inline fun debugLog(subtag: String, block: () -> String = { "" }) {
121+
private inline fun debugLog(subtag: String, block: () -> String = { "" }) {
179122
if (IS_DEBUG_BUILD && ReactNativeFeatureFlags.enableVirtualViewDebugFeatures()) {
180123
FLog.d("$DEBUG_TAG:$subtag", block())
181124
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.scroll
9+
10+
import android.view.ViewGroup
11+
import com.facebook.common.logging.FLog
12+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
13+
import com.facebook.react.views.virtual.VirtualViewMode
14+
15+
internal class VirtualViewContainerStateClassic(scrollView: ViewGroup) :
16+
VirtualViewContainerState(scrollView) {
17+
18+
// Provide the concrete implementation for the abstract virtualViews property
19+
override val virtualViews: MutableCollection<VirtualView> = mutableSetOf()
20+
21+
// Implement the abstract updateModes method
22+
override fun updateModes(virtualView: VirtualView?) {
23+
scrollView.getDrawingRect(visibleRect)
24+
25+
if (visibleRect.isEmpty()) {
26+
debugLog("updateModes", { "scrollView visibleRect is empty" })
27+
return
28+
}
29+
30+
prerenderRect.set(visibleRect)
31+
prerenderRect.inset(
32+
(-prerenderRect.width() * prerenderRatio).toInt(),
33+
(-prerenderRect.height() * prerenderRatio).toInt(),
34+
)
35+
36+
if (hysteresisRatio > 0.0) {
37+
hysteresisRect.set(prerenderRect)
38+
hysteresisRect.inset(
39+
(-visibleRect.width() * hysteresisRatio).toInt(),
40+
(-visibleRect.height() * hysteresisRatio).toInt(),
41+
)
42+
}
43+
44+
val virtualViewsIt =
45+
if (virtualView != null) listOf(virtualView) else virtualViews.toMutableSet()
46+
virtualViewsIt.forEach { vv ->
47+
val rect = vv.containerRelativeRect
48+
49+
var mode: VirtualViewMode? = VirtualViewMode.Hidden
50+
var thresholdRect = emptyRect
51+
when {
52+
rectsOverlap(rect, visibleRect) -> {
53+
thresholdRect = visibleRect
54+
if (onWindowFocusChangeListener != null) {
55+
if (scrollView.hasWindowFocus()) {
56+
mode = VirtualViewMode.Visible
57+
} else {
58+
mode = VirtualViewMode.Prerender
59+
}
60+
} else {
61+
mode = VirtualViewMode.Visible
62+
}
63+
}
64+
rectsOverlap(rect, prerenderRect) -> {
65+
mode = VirtualViewMode.Prerender
66+
thresholdRect = prerenderRect
67+
}
68+
(hysteresisRatio > 0.0 && rectsOverlap(rect, hysteresisRect)) -> {
69+
mode = null
70+
}
71+
}
72+
73+
if (mode != null) {
74+
vv.onModeChange(mode, thresholdRect)
75+
debugLog(
76+
"updateModes",
77+
{
78+
"virtualView=${vv.virtualViewID} mode=$mode rect=$rect thresholdRect=$thresholdRect"
79+
},
80+
)
81+
}
82+
}
83+
}
84+
}
85+
86+
private const val DEBUG_TAG: String = "VirtualViewContainerStateClassic"
87+
88+
private inline fun debugLog(subtag: String, block: () -> String = { "" }) {
89+
if (IS_DEBUG_BUILD && ReactNativeFeatureFlags.enableVirtualViewDebugFeatures()) {
90+
FLog.d("$DEBUG_TAG:$subtag", block())
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.scroll
9+
10+
import android.view.ViewGroup
11+
import com.facebook.common.logging.FLog
12+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
13+
import com.facebook.react.views.virtual.VirtualViewMode
14+
15+
/**
16+
* A class that implements VirtualViewContainerState with a more efficient updateModes() algorithm.
17+
* This has been filled out with the Classic implementation for now to support the new Factory
18+
* creation method.
19+
*/
20+
internal class VirtualViewContainerStateExperimental(scrollView: ViewGroup) :
21+
VirtualViewContainerState(scrollView) {
22+
23+
// Provide the concrete implementation for the abstract virtualViews property
24+
override val virtualViews: MutableCollection<VirtualView> = mutableSetOf()
25+
26+
// Implement the abstract updateModes method
27+
override fun updateModes(virtualView: VirtualView?) {
28+
scrollView.getDrawingRect(visibleRect)
29+
30+
if (visibleRect.isEmpty()) {
31+
debugLog("updateModes", { "scrollView visibleRect is empty" })
32+
return
33+
}
34+
35+
prerenderRect.set(visibleRect)
36+
prerenderRect.inset(
37+
(-prerenderRect.width() * prerenderRatio).toInt(),
38+
(-prerenderRect.height() * prerenderRatio).toInt(),
39+
)
40+
41+
if (hysteresisRatio > 0.0) {
42+
hysteresisRect.set(prerenderRect)
43+
hysteresisRect.inset(
44+
(-visibleRect.width() * hysteresisRatio).toInt(),
45+
(-visibleRect.height() * hysteresisRatio).toInt(),
46+
)
47+
}
48+
49+
val virtualViewsIt =
50+
if (virtualView != null) listOf(virtualView) else virtualViews.toMutableSet()
51+
virtualViewsIt.forEach { vv ->
52+
val rect = vv.containerRelativeRect
53+
54+
var mode: VirtualViewMode? = VirtualViewMode.Hidden
55+
var thresholdRect = emptyRect
56+
when {
57+
rectsOverlap(rect, visibleRect) -> {
58+
thresholdRect = visibleRect
59+
if (onWindowFocusChangeListener != null) {
60+
if (scrollView.hasWindowFocus()) {
61+
mode = VirtualViewMode.Visible
62+
} else {
63+
mode = VirtualViewMode.Prerender
64+
}
65+
} else {
66+
mode = VirtualViewMode.Visible
67+
}
68+
}
69+
rectsOverlap(rect, prerenderRect) -> {
70+
mode = VirtualViewMode.Prerender
71+
thresholdRect = prerenderRect
72+
}
73+
(hysteresisRatio > 0.0 && rectsOverlap(rect, hysteresisRect)) -> {
74+
mode = null
75+
}
76+
}
77+
78+
if (mode != null) {
79+
vv.onModeChange(mode, thresholdRect)
80+
debugLog(
81+
"updateModes",
82+
{
83+
"virtualView=${vv.virtualViewID} mode=$mode rect=$rect thresholdRect=$thresholdRect"
84+
},
85+
)
86+
}
87+
}
88+
}
89+
}
90+
91+
private const val DEBUG_TAG: String = "VirtualViewContainerStateExperimental"
92+
93+
private inline fun debugLog(subtag: String, block: () -> String = { "" }) {
94+
if (IS_DEBUG_BUILD && ReactNativeFeatureFlags.enableVirtualViewDebugFeatures()) {
95+
FLog.d("$DEBUG_TAG:$subtag", block())
96+
}
97+
}

0 commit comments

Comments
 (0)