|
| 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.uimanager |
| 9 | + |
| 10 | +import android.view.ViewParent |
| 11 | +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags |
| 12 | + |
| 13 | +/** |
| 14 | + * Implemented by views that cache whether their subtree contains a descendant with `elevation`, so |
| 15 | + * the offscreen-compositing decision in `ReactViewGroup.hasOverlappingRendering()` is O(1) at draw |
| 16 | + * time (see https://github.com/react/react-native/issues/23090). |
| 17 | + * |
| 18 | + * The cache is marked stale only from logical mount/unmount and elevation changes (via |
| 19 | + * [invalidateAncestors]), never from the subview-clipping/scroll path, so scrolling a |
| 20 | + * `removeClippedSubviews` list does no per-frame work. Invalidation is lazy -- it only flips a flag |
| 21 | + * and the subtree is rescanned once, on the next query -- so mounting N children costs O(N) marks |
| 22 | + * rather than O(N^2) rescans. The cache is self-correcting and cannot drift like a maintained |
| 23 | + * counter. |
| 24 | + * |
| 25 | + * Mutation paths that change children without going through `ViewGroupManager` / |
| 26 | + * `ReactClippingViewManager` or `BaseViewManager.setElevation` (a view manager overriding |
| 27 | + * `addView`/`removeViewAt` without calling super, or re-routing children to another container) do |
| 28 | + * not invalidate ancestor caches; the worst case is a briefly stale flag until the next tracked |
| 29 | + * change, never a leak or drift. |
| 30 | + * |
| 31 | + * One known limitation: an invalidation that *originates inside a subtree currently clipped out of |
| 32 | + * a `removeClippedSubviews` container* cannot reach that container. [invalidateAncestors] walks up |
| 33 | + * via `View.getParent()`, which is null for a clipped-out (detached) node, so the walk stops at the |
| 34 | + * detached subtree root before reaching the logical container that still holds it in `allChildren`. |
| 35 | + * This affects both invalidation sources: `BaseViewManager.setElevation` (an elevation change on a |
| 36 | + * clipped-out view or its descendant) and the `ViewGroupManager.addView`/`removeViewAt` mount hooks |
| 37 | + * (mounting/unmounting an elevated descendant under a clipped-out sub-container). In both cases the |
| 38 | + * container's cache stays briefly stale until the next tracked change to its own direct children -- |
| 39 | + * which any sibling mount/unmount during scrolling provides -- so the shadow may only briefly fail |
| 40 | + * to composite offscreen in this narrow scenario. |
| 41 | + */ |
| 42 | +internal interface HasElevatedDescendantCache { |
| 43 | + /** |
| 44 | + * Marks this view's cached elevated-descendant flag stale. The value is recomputed lazily on the |
| 45 | + * next `hasOverlappingRendering()` query, so this stays O(1) on the mount/unmount path. Returns |
| 46 | + * `true` if the cache was valid and is now newly invalidated, or `false` if it was already stale |
| 47 | + * -- in which case its ancestors were already invalidated too, so callers walking up can stop. |
| 48 | + */ |
| 49 | + fun invalidateElevatedDescendantCache(): Boolean |
| 50 | + |
| 51 | + companion object { |
| 52 | + /** |
| 53 | + * Marks the elevated-descendant cache stale on [start] and each ancestor, walking through any |
| 54 | + * intervening non-implementing container. This is O(depth) of flag writes; the actual subtree |
| 55 | + * scan happens once, lazily, at the next draw. A no-op unless the feature flag is enabled, so |
| 56 | + * the flag-off path costs nothing. |
| 57 | + */ |
| 58 | + @JvmStatic |
| 59 | + fun invalidateAncestors(start: ViewParent?) { |
| 60 | + if (!ReactNativeFeatureFlags.enableAndroidAutoOffscreenCompositingForElevation()) { |
| 61 | + return |
| 62 | + } |
| 63 | + var node: ViewParent? = start |
| 64 | + while (node != null) { |
| 65 | + val cache = node as? HasElevatedDescendantCache |
| 66 | + if (cache != null && !cache.invalidateElevatedDescendantCache()) { |
| 67 | + // Already stale: whatever invalidated it already walked up and invalidated its ancestors. |
| 68 | + break |
| 69 | + } |
| 70 | + node = node.parent |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | +} |
0 commit comments