Skip to content
Open
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
3 changes: 2 additions & 1 deletion packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -6434,7 +6434,7 @@ public final class com/facebook/react/views/view/ReactDrawableHelper {
public static final fun createDrawableFromJSDescription (Landroid/content/Context;Lcom/facebook/react/bridge/ReadableMap;)Landroid/graphics/drawable/Drawable;
}

public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGroup, com/facebook/react/touch/ReactHitSlopView, com/facebook/react/touch/ReactInterceptingViewGroup, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/uimanager/ReactPointerEventsView {
public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGroup, com/facebook/react/touch/ReactHitSlopView, com/facebook/react/touch/ReactInterceptingViewGroup, com/facebook/react/uimanager/HasElevatedDescendantCache, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/uimanager/ReactPointerEventsView {
public fun <init> (Landroid/content/Context;)V
public fun addChildrenForAccessibility (Ljava/util/ArrayList;)V
public final fun cleanUpAxOrderListener ()V
Expand All @@ -6455,6 +6455,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro
public fun getPointerEvents ()Lcom/facebook/react/uimanager/PointerEvents;
public fun getRemoveClippedSubviews ()Z
public fun hasOverlappingRendering ()Z
public fun invalidateElevatedDescendantCache ()Z
protected fun onAttachedToWindow ()V
protected fun onConfigurationChanged (Landroid/content/res/Configuration;)V
public fun onHoverEvent (Landroid/view/MotionEvent;)Z
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<<de19deb8eaa373fc2048f5a9453674b7>>
* @generated SignedSource<<7d7547b5f25dbe0d0e6722d2f2bf5baf>>
*/

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

/**
* When enabled, a View with reduced opacity that contains an elevated descendant is composited offscreen so the elevation shadow fades uniformly instead of rendering as banded per-primitive alpha.
*/
@JvmStatic
public fun enableAndroidAutoOffscreenCompositingForElevation(): Boolean = accessor.enableAndroidAutoOffscreenCompositingForElevation()

/**
* Enables various optimizations throughout the path of measuring text on Android.
*/
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<<a1c52a57a980eaab23d92416340a6734>>
* @generated SignedSource<<c07cb2f4489ba94f267d0c76616ee718>>
*/

/**
Expand Down Expand Up @@ -32,6 +32,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var disableViewPreallocationAndroidCache: Boolean? = null
private var enableAccessibilityOrderCache: Boolean? = null
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
private var enableAndroidAutoOffscreenCompositingForElevationCache: Boolean? = null
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
private var enableBridgelessArchitectureCache: Boolean? = null
private var enableBufferedCallInvokerCache: Boolean? = null
Expand Down Expand Up @@ -216,6 +217,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}

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

override fun enableAndroidTextMeasurementOptimizations(): Boolean {
var cached = enableAndroidTextMeasurementOptimizationsCache
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<<8a0d1f134b175ada7550499896c09203>>
* @generated SignedSource<<7d6ce76213f08ecb356fa68e77edfe84>>
*/

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

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

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

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

@DoNotStrip @JvmStatic public external fun enableBridgelessArchitecture(): 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<<891abee69433c105d062b45ed357fd5c>>
* @generated SignedSource<<0c228a0f53ced43d6744551a92d0ed86>>
*/

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

override fun enableAccumulatedUpdatesInRawPropsAndroid(): Boolean = false

override fun enableAndroidAutoOffscreenCompositingForElevation(): Boolean = false

override fun enableAndroidTextMeasurementOptimizations(): Boolean = false

override fun enableBridgelessArchitecture(): 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<<4c62625780d9f767714f8481c7496342>>
* @generated SignedSource<<37c9d46dfd98d868601390a9262f8d5a>>
*/

/**
Expand Down Expand Up @@ -36,6 +36,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var disableViewPreallocationAndroidCache: Boolean? = null
private var enableAccessibilityOrderCache: Boolean? = null
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
private var enableAndroidAutoOffscreenCompositingForElevationCache: Boolean? = null
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
private var enableBridgelessArchitectureCache: Boolean? = null
private var enableBufferedCallInvokerCache: Boolean? = null
Expand Down Expand Up @@ -232,6 +233,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}

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

override fun enableAndroidTextMeasurementOptimizations(): Boolean {
var cached = enableAndroidTextMeasurementOptimizationsCache
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<<e991f910f4be27f532c6b81c436bcdf1>>
* @generated SignedSource<<18ffa42c9d28304df99548a350b014ac>>
*/

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

@DoNotStrip public fun enableAccumulatedUpdatesInRawPropsAndroid(): Boolean

@DoNotStrip public fun enableAndroidAutoOffscreenCompositingForElevation(): Boolean

@DoNotStrip public fun enableAndroidTextMeasurementOptimizations(): Boolean

@DoNotStrip public fun enableBridgelessArchitecture(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,6 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) {
view.setFocusable(false);
view.setFocusableInTouchMode(false);

// https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-mainline-12.0.0_r96/core/java/android/view/View.java#5491
view.setElevation(0);

// Predictably, alpha defaults to 1:
// https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#2186
// This accounts for resetting mBackfaceOpacity and mBackfaceVisibility
Expand Down Expand Up @@ -274,7 +271,12 @@ public void setOpacity(@NonNull T view, float opacity) {

@ReactProp(name = ViewProps.ELEVATION)
public void setElevation(@NonNull T view, float elevation) {
ViewCompat.setElevation(view, PixelUtil.toPixelFromDIP(elevation));
float px = PixelUtil.toPixelFromDIP(elevation);
boolean wasElevated = view.getElevation() > 0f;
ViewCompat.setElevation(view, px);
if (wasElevated != (px > 0f)) {
HasElevatedDescendantCache.invalidateAncestors(view.getParent());
}
}

@ReactProp(name = ViewProps.SHADOW_COLOR, defaultInt = Color.BLACK, customType = "Color")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.uimanager

import android.view.ViewParent
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags

/**
* Implemented by views that cache whether their subtree contains a descendant with `elevation`, so
* the offscreen-compositing decision in `ReactViewGroup.hasOverlappingRendering()` is O(1) at draw
* time (see https://github.com/react/react-native/issues/23090).
*
* The cache is marked stale only from logical mount/unmount and elevation changes (via
* [invalidateAncestors]), never from the subview-clipping/scroll path, so scrolling a
* `removeClippedSubviews` list does no per-frame work. Invalidation is lazy -- it only flips a flag
* and the subtree is rescanned once, on the next query -- so mounting N children costs O(N) marks
* rather than O(N^2) rescans. The cache is self-correcting and cannot drift like a maintained
* counter.
*
* Mutation paths that change children without going through `ViewGroupManager` /
* `ReactClippingViewManager` or `BaseViewManager.setElevation` (a view manager overriding
* `addView`/`removeViewAt` without calling super, or re-routing children to another container) do
* not invalidate ancestor caches; the worst case is a briefly stale flag until the next tracked
* change, never a leak or drift.
*
* One known limitation: an invalidation that *originates inside a subtree currently clipped out of
* a `removeClippedSubviews` container* cannot reach that container. [invalidateAncestors] walks up
* via `View.getParent()`, which is null for a clipped-out (detached) node, so the walk stops at the
* detached subtree root before reaching the logical container that still holds it in `allChildren`.
* This affects both invalidation sources: `BaseViewManager.setElevation` (an elevation change on a
* clipped-out view or its descendant) and the `ViewGroupManager.addView`/`removeViewAt` mount hooks
* (mounting/unmounting an elevated descendant under a clipped-out sub-container). In both cases the
* container's cache stays briefly stale until the next tracked change to its own direct children --
* which any sibling mount/unmount during scrolling provides -- so the shadow may only briefly fail
* to composite offscreen in this narrow scenario.
*/
internal interface HasElevatedDescendantCache {
/**
* Marks this view's cached elevated-descendant flag stale. The value is recomputed lazily on the
* next `hasOverlappingRendering()` query, so this stays O(1) on the mount/unmount path. Returns
* `true` if the cache was valid and is now newly invalidated, or `false` if it was already stale
* -- in which case its ancestors were already invalidated too, so callers walking up can stop.
*/
fun invalidateElevatedDescendantCache(): Boolean

companion object {
/**
* Marks the elevated-descendant cache stale on [start] and each ancestor, walking through any
* intervening non-implementing container. This is O(depth) of flag writes; the actual subtree
* scan happens once, lazily, at the next draw. A no-op unless the feature flag is enabled, so
* the flag-off path costs nothing.
*/
@JvmStatic
fun invalidateAncestors(start: ViewParent?) {
if (!ReactNativeFeatureFlags.enableAndroidAutoOffscreenCompositingForElevation()) {
return
}
var node: ViewParent? = start
while (node != null) {
val cache = node as? HasElevatedDescendantCache
if (cache != null && !cache.invalidateElevatedDescendantCache()) {
// Already stale: whatever invalidated it already walked up and invalidated its ancestors.
break
}
node = node.parent
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ constructor(reactContext: ReactApplicationContext? = null) :

public override fun updateExtraData(root: T, extraData: Any): Unit = Unit

public override fun addView(parent: T, child: View, index: Int): Unit =
parent.addView(child, index)
public override fun addView(parent: T, child: View, index: Int) {
parent.addView(child, index)
HasElevatedDescendantCache.invalidateAncestors(parent)
}

/**
* Convenience method for batching a set of addView calls Note that this adds the views to the
Expand All @@ -47,6 +49,7 @@ constructor(reactContext: ReactApplicationContext? = null) :
public override fun removeViewAt(parent: T, index: Int) {
UiThreadUtil.assertOnUiThread()
parent.removeViewAt(index)
HasElevatedDescendantCache.invalidateAncestors(parent)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package com.facebook.react.views.view

import android.view.View
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.uimanager.HasElevatedDescendantCache
import com.facebook.react.uimanager.ReactClippingViewGroupHelper
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.annotations.ReactProp
Expand All @@ -35,6 +36,7 @@ public abstract class ReactClippingViewManager<T : ReactViewGroup> : ViewGroupMa
} else {
parent.addView(child, index)
}
HasElevatedDescendantCache.invalidateAncestors(parent)
}

override fun getChildCount(parent: T): Int {
Expand Down Expand Up @@ -67,6 +69,7 @@ public abstract class ReactClippingViewManager<T : ReactViewGroup> : ViewGroupMa
} else {
parent.removeViewAt(index)
}
HasElevatedDescendantCache.invalidateAncestors(parent)
}

override fun removeAllViews(parent: T) {
Expand All @@ -78,5 +81,6 @@ public abstract class ReactClippingViewManager<T : ReactViewGroup> : ViewGroupMa
} else {
parent.removeAllViews()
}
HasElevatedDescendantCache.invalidateAncestors(parent)
}
}
Loading