Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<132ff30c4a5ecf6b38dd0d6cc47d3abc>>
* @generated SignedSource<<2ac9938108dfe555fc7e7d875cc21987>>
*/

/**
Expand Down Expand Up @@ -312,6 +312,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableViewRecyclingForView(): Boolean = accessor.enableViewRecyclingForView()

/**
* Enables the experimental version of `VirtualViewContainerState`.
*/
@JvmStatic
public fun enableVirtualViewContainerStateExperimental(): Boolean = accessor.enableVirtualViewContainerStateExperimental()

/**
* Enables VirtualView debug features such as logging and overlays.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<38838d89c61124afce1f13045593aeb4>>
* @generated SignedSource<<3cb451816d08f2f3cefb757f1e2ce72a>>
*/

/**
Expand Down Expand Up @@ -67,6 +67,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var enableViewRecyclingForScrollViewCache: Boolean? = null
private var enableViewRecyclingForTextCache: Boolean? = null
private var enableViewRecyclingForViewCache: Boolean? = null
private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null
private var enableVirtualViewDebugFeaturesCache: Boolean? = null
private var enableVirtualViewRenderStateCache: Boolean? = null
private var enableVirtualViewWindowFocusDetectionCache: Boolean? = null
Expand Down Expand Up @@ -524,6 +525,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enableVirtualViewContainerStateExperimental(): Boolean {
var cached = enableVirtualViewContainerStateExperimentalCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableVirtualViewContainerStateExperimental()
enableVirtualViewContainerStateExperimentalCache = cached
}
return cached
}

override fun enableVirtualViewDebugFeatures(): Boolean {
var cached = enableVirtualViewDebugFeaturesCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<ef75a380b395d88cb0551d3a44d9961d>>
* @generated SignedSource<<de0fe8d116ca48e918f67c4c32f1183f>>
*/

/**
Expand Down Expand Up @@ -122,6 +122,8 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun enableViewRecyclingForView(): Boolean

@DoNotStrip @JvmStatic public external fun enableVirtualViewContainerStateExperimental(): Boolean

@DoNotStrip @JvmStatic public external fun enableVirtualViewDebugFeatures(): Boolean

@DoNotStrip @JvmStatic public external fun enableVirtualViewRenderState(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<c601ddc9bd62282f8079d0cb3578675d>>
* @generated SignedSource<<674f7910fe5dcd750ba2661122c82670>>
*/

/**
Expand Down Expand Up @@ -117,6 +117,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun enableViewRecyclingForView(): Boolean = true

override fun enableVirtualViewContainerStateExperimental(): Boolean = false

override fun enableVirtualViewDebugFeatures(): Boolean = false

override fun enableVirtualViewRenderState(): Boolean = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<2fa1e7cd2e1d4009dfa09a5fd27a872a>>
* @generated SignedSource<<5facf1328467d62b41dd9f82c5111882>>
*/

/**
Expand Down Expand Up @@ -71,6 +71,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var enableViewRecyclingForScrollViewCache: Boolean? = null
private var enableViewRecyclingForTextCache: Boolean? = null
private var enableViewRecyclingForViewCache: Boolean? = null
private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null
private var enableVirtualViewDebugFeaturesCache: Boolean? = null
private var enableVirtualViewRenderStateCache: Boolean? = null
private var enableVirtualViewWindowFocusDetectionCache: Boolean? = null
Expand Down Expand Up @@ -575,6 +576,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}

override fun enableVirtualViewContainerStateExperimental(): Boolean {
var cached = enableVirtualViewContainerStateExperimentalCache
if (cached == null) {
cached = currentProvider.enableVirtualViewContainerStateExperimental()
accessedFeatureFlags.add("enableVirtualViewContainerStateExperimental")
enableVirtualViewContainerStateExperimentalCache = cached
}
return cached
}

override fun enableVirtualViewDebugFeatures(): Boolean {
var cached = enableVirtualViewDebugFeaturesCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<901e5678bff081bcb6b8e2d46364b977>>
* @generated SignedSource<<5a9a51638a72bda657f312260bb1297a>>
*/

/**
Expand Down Expand Up @@ -117,6 +117,8 @@ public interface ReactNativeFeatureFlagsProvider {

@DoNotStrip public fun enableViewRecyclingForView(): Boolean

@DoNotStrip public fun enableVirtualViewContainerStateExperimental(): Boolean

@DoNotStrip public fun enableVirtualViewDebugFeatures(): Boolean

@DoNotStrip public fun enableVirtualViewRenderState(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ private void updateView() {}
@Override
public VirtualViewContainerState getVirtualViewContainerState() {
if (mVirtualViewContainerState == null) {
mVirtualViewContainerState = new VirtualViewContainerState(this);
mVirtualViewContainerState = VirtualViewContainerState.create(this);
}

return mVirtualViewContainerState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ private void updateView() {}
@Override
public VirtualViewContainerState getVirtualViewContainerState() {
if (mVirtualViewContainerState == null) {
mVirtualViewContainerState = new VirtualViewContainerState(this);
mVirtualViewContainerState = VirtualViewContainerState.create(this);
}

return mVirtualViewContainerState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ internal interface VirtualViewContainer {

public interface VirtualView {
public val virtualViewID: String

public val containerRelativeRect: Rect

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

internal class VirtualViewContainerState {

private val prerenderRatio: Double = ReactNativeFeatureFlags.virtualViewPrerenderRatio()
private val hysteresisRatio: Double = ReactNativeFeatureFlags.virtualViewHysteresisRatio()

private val virtualViews: MutableSet<VirtualView> = mutableSetOf()
private val emptyRect: Rect = Rect()
private val visibleRect: Rect = Rect()
private val prerenderRect: Rect = Rect()
private val hysteresisRect: Rect = Rect()
private val onWindowFocusChangeListener =
internal abstract class VirtualViewContainerState {
protected val prerenderRatio: Double = ReactNativeFeatureFlags.virtualViewPrerenderRatio()
protected val hysteresisRatio: Double = ReactNativeFeatureFlags.virtualViewHysteresisRatio()
protected abstract val virtualViews: MutableCollection<VirtualView>
protected val emptyRect: Rect = Rect()
protected val visibleRect: Rect = Rect()
protected val prerenderRect: Rect = Rect()
protected val hysteresisRect: Rect = Rect()
protected val onWindowFocusChangeListener =
if (ReactNativeFeatureFlags.enableVirtualViewWindowFocusDetection()) {
ViewTreeObserver.OnWindowFocusChangeListener {
debugLog("onWindowFocusChanged")
Expand All @@ -66,8 +63,18 @@ internal class VirtualViewContainerState {
} else {
null
}
protected val scrollView: ViewGroup

private val scrollView: ViewGroup
companion object {
@JvmStatic
fun create(scrollView: ViewGroup): VirtualViewContainerState {
return if (ReactNativeFeatureFlags.enableVirtualViewContainerStateExperimental()) {
VirtualViewContainerStateExperimental(scrollView)
} else {
VirtualViewContainerStateClassic(scrollView)
}
}
}

constructor(scrollView: ViewGroup) {
this.scrollView = scrollView
Expand All @@ -76,13 +83,13 @@ internal class VirtualViewContainerState {
}
}

public fun cleanup() {
fun cleanup() {
if (onWindowFocusChangeListener != null) {
scrollView.viewTreeObserver.removeOnWindowFocusChangeListener(onWindowFocusChangeListener)
}
}

public fun onChange(virtualView: VirtualView) {
open fun onChange(virtualView: VirtualView) {
if (virtualViews.add(virtualView)) {
debugLog("add", { "virtualViewID=${virtualView.virtualViewID}" })
} else {
Expand All @@ -91,91 +98,27 @@ internal class VirtualViewContainerState {
updateModes(virtualView)
}

public fun remove(virtualView: VirtualView) {
open fun remove(virtualView: VirtualView) {
assert(virtualViews.remove(virtualView)) {
"Attempting to remove non-existent VirtualView: ${virtualView.virtualViewID}"
}
debugLog("remove", { "virtualViewID=${virtualView.virtualViewID}" })
}

// Called on ScrollView onLayout or onScroll
public fun updateState() {
fun updateState() {
debugLog("updateState")
updateModes()
}

private fun updateModes(virtualView: VirtualView? = null) {
scrollView.getDrawingRect(visibleRect)

// This happens because ScrollView content isn't ready yet. The danger here is if ScrollView
// intentionally goes but curently ScrollView and v1 Fling use this check to determine if
// "content ready"
if (visibleRect.isEmpty()) {
debugLog("updateModes", { "scrollView visibleRect is empty" })
return
}

prerenderRect.set(visibleRect)
prerenderRect.inset(
(-prerenderRect.width() * prerenderRatio).toInt(),
(-prerenderRect.height() * prerenderRatio).toInt(),
)

if (hysteresisRatio > 0.0) {
hysteresisRect.set(prerenderRect)
hysteresisRect.inset(
(-visibleRect.width() * hysteresisRatio).toInt(),
(-visibleRect.height() * hysteresisRatio).toInt(),
)
}

val virtualViewsIt =
if (virtualView != null) listOf(virtualView) else virtualViews.toMutableSet()
virtualViewsIt.forEach { vv ->
val rect = vv.containerRelativeRect

var mode: VirtualViewMode? = VirtualViewMode.Hidden
var thresholdRect = emptyRect
when {
rectsOverlap(rect, visibleRect) -> {
thresholdRect = visibleRect
if (onWindowFocusChangeListener != null) {
if (scrollView.hasWindowFocus()) {
mode = VirtualViewMode.Visible
} else {
mode = VirtualViewMode.Prerender
}
} else {
mode = VirtualViewMode.Visible
}
}
rectsOverlap(rect, prerenderRect) -> {
mode = VirtualViewMode.Prerender
thresholdRect = prerenderRect
}
(hysteresisRatio > 0.0 && rectsOverlap(rect, hysteresisRect)) -> {
mode = null
}
}

if (mode != null) {
vv.onModeChange(mode, thresholdRect)
debugLog(
"updateModes",
{
"virtualView=${vv.virtualViewID} mode=$mode rect=$rect thresholdRect=$thresholdRect"
},
)
}
}
}
protected abstract fun updateModes(virtualView: VirtualView? = null)
}

private const val DEBUG_TAG: String = "VirtualViewContainerState"
private val IS_DEBUG_BUILD =
internal val IS_DEBUG_BUILD =
ReactBuildConfig.DEBUG || ReactBuildConfig.IS_INTERNAL_BUILD || ReactBuildConfig.ENABLE_PERFETTO

internal inline fun debugLog(subtag: String, block: () -> String = { "" }) {
private inline fun debugLog(subtag: String, block: () -> String = { "" }) {
if (IS_DEBUG_BUILD && ReactNativeFeatureFlags.enableVirtualViewDebugFeatures()) {
FLog.d("$DEBUG_TAG:$subtag", block())
}
Expand Down
Loading
Loading