Skip to content

Commit fd4b60b

Browse files
zeyapmeta-codesync[bot]
authored andcommitted
Android bitmap snapshot capture and display
Summary: ## Changelog: [Internal] [Added] - Android bitmap snapshot capture and display Implement the Android side of view transition bitmap snapshots. **`ViewTransitionSnapshotManager`** (new Kotlin class): Manages the full snapshot lifecycle as a `UIManagerListener`. Captures bitmaps on the UI thread, maps them from source to target pseudo-element tags, re-applies after mount cycles (since views may be recreated), and self-cleans when views are deleted. **JNI bridge:** `FabricUIManagerBinding` → `FabricMountingManager` → `FabricUIManager` JNI delegates to the snapshot manager. **`SurfaceMountingManager.applyViewSnapshot`:** sets bitmap as view background using the KTX `Bitmap.toDrawable` extension. Reviewed By: sammy-SC, NickGerleman Differential Revision: D99173446
1 parent cbd527b commit fd4b60b

9 files changed

Lines changed: 273 additions & 3 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,6 +2275,7 @@ public final class com/facebook/react/fabric/FabricUIManagerProviderImpl : com/f
22752275

22762276
public final class com/facebook/react/fabric/mounting/SurfaceMountingManager {
22772277
public final fun addViewAt (III)V
2278+
public final fun applyViewSnapshot (ILandroid/graphics/Bitmap;)V
22782279
public final fun attachRootView (Landroid/view/View;Lcom/facebook/react/uimanager/ThemedReactContext;)V
22792280
public final fun deleteView (I)V
22802281
public final fun enqueuePendingEvent (ILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ public class FabricUIManager
208208

209209
private boolean mDriveCxxAnimations = false;
210210

211+
private @Nullable ViewTransitionSnapshotManager mViewTransitionSnapshotManager;
212+
211213
private long mDispatchViewUpdatesTime = 0l;
212214
private long mCommitStartTime = 0l;
213215
private long mLayoutTime = 0l;
@@ -811,6 +813,40 @@ public void synchronouslyUpdateViewOnUIThread(final int reactTag, final Readable
811813
ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber);
812814
}
813815

816+
/** Called from C++ via JNI. */
817+
@SuppressLint("NotInvokedPrivateMethod")
818+
@SuppressWarnings("unused")
819+
@AnyThread
820+
@ThreadConfined(ANY)
821+
private void captureViewSnapshot(final int reactTag, final int surfaceId) {
822+
getViewTransitionSnapshotManager().captureViewSnapshot(reactTag, surfaceId);
823+
}
824+
825+
/** Called from C++ via JNI. */
826+
@SuppressLint("NotInvokedPrivateMethod")
827+
@SuppressWarnings("unused")
828+
@AnyThread
829+
@ThreadConfined(ANY)
830+
private void setViewSnapshot(final int sourceTag, final int targetTag, final int surfaceId) {
831+
getViewTransitionSnapshotManager().setViewSnapshot(sourceTag, targetTag);
832+
}
833+
834+
/** Called from C++ via JNI. */
835+
@SuppressLint("NotInvokedPrivateMethod")
836+
@SuppressWarnings("unused")
837+
@AnyThread
838+
@ThreadConfined(ANY)
839+
private void clearPendingSnapshots() {
840+
getViewTransitionSnapshotManager().clearPendingSnapshots();
841+
}
842+
843+
private synchronized ViewTransitionSnapshotManager getViewTransitionSnapshotManager() {
844+
if (mViewTransitionSnapshotManager == null) {
845+
mViewTransitionSnapshotManager = new ViewTransitionSnapshotManager(this, mMountingManager);
846+
}
847+
return mViewTransitionSnapshotManager;
848+
}
849+
814850
@SuppressLint("NotInvokedPrivateMethod")
815851
@SuppressWarnings("unused")
816852
@AnyThread
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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.fabric
9+
10+
import android.graphics.Bitmap
11+
import android.graphics.Canvas
12+
import android.graphics.Rect
13+
import android.os.Build
14+
import android.os.Handler
15+
import android.os.Looper
16+
import android.view.PixelCopy
17+
import android.view.View
18+
import android.view.Window
19+
import androidx.annotation.RequiresApi
20+
import androidx.annotation.UiThread
21+
import androidx.core.graphics.createBitmap
22+
import com.facebook.infer.annotation.ThreadConfined
23+
import com.facebook.react.bridge.UIManager
24+
import com.facebook.react.bridge.UIManagerListener
25+
import com.facebook.react.bridge.UiThreadUtil
26+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
27+
import com.facebook.react.fabric.mounting.MountingManager
28+
29+
/**
30+
* Manages bitmap snapshots of views during view transitions. Captures bitmaps from old views and
31+
* applies them to pseudo-element shadow nodes, re-applying after each mount cycle since views may
32+
* be recreated. Cleans up entries whose views have been deleted.
33+
*/
34+
@OptIn(UnstableReactNativeAPI::class)
35+
internal class ViewTransitionSnapshotManager(
36+
private val uiManager: FabricUIManager,
37+
private val mountingManager: MountingManager,
38+
) : UIManagerListener {
39+
40+
companion object {
41+
private fun captureSoftwareBitmap(view: View): Bitmap {
42+
val bitmap = createBitmap(view.width, view.height)
43+
view.draw(Canvas(bitmap))
44+
return bitmap
45+
}
46+
}
47+
48+
// Captured bitmaps keyed by source tag. Populated by onBitmapCaptured.
49+
@ThreadConfined(ThreadConfined.UI) private val viewSnapshots = LinkedHashMap<Int, Bitmap>()
50+
51+
// Source→target tag mapping. Populated by setViewSnapshot.
52+
// A snapshot is resolved when both maps contain an entry for the same source tag.
53+
@ThreadConfined(ThreadConfined.UI) private val pendingTargets = LinkedHashMap<Int, Int>()
54+
55+
@ThreadConfined(ThreadConfined.UI) private var listenerRegistered = false
56+
57+
private val mainHandler = Handler(Looper.getMainLooper())
58+
59+
@UiThread
60+
private fun onBitmapCaptured(reactTag: Int, bitmap: Bitmap) {
61+
viewSnapshots[reactTag] = bitmap
62+
if (reactTag in pendingTargets) {
63+
ensureListenerRegistered()
64+
}
65+
}
66+
67+
@UiThread
68+
private fun ensureListenerRegistered() {
69+
if (!listenerRegistered) {
70+
listenerRegistered = true
71+
uiManager.addUIManagerEventListener(this)
72+
}
73+
}
74+
75+
/**
76+
* Captures a bitmap snapshot of the view identified by the given tag. On API 26+, uses PixelCopy
77+
* to capture directly from the GPU-composited surface (faster for complex views, captures
78+
* hardware-accelerated content). Falls back to View.draw() on older APIs.
79+
*/
80+
fun captureViewSnapshot(reactTag: Int, surfaceId: Int) {
81+
UiThreadUtil.runOnUiThread {
82+
val smm = mountingManager.getSurfaceManager(surfaceId) ?: return@runOnUiThread
83+
if (!smm.getViewExists(reactTag)) return@runOnUiThread
84+
val view = smm.getView(reactTag)
85+
if (view.width <= 0 || view.height <= 0) return@runOnUiThread
86+
87+
val window =
88+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
89+
(view.context as? com.facebook.react.bridge.ReactContext)?.getCurrentActivity()?.window
90+
} else {
91+
null
92+
}
93+
94+
if (window != null) {
95+
captureHardwareBitmap(view, reactTag, window)
96+
} else {
97+
// Software fallback runs synchronously, so onBitmapCaptured always
98+
// completes before setViewSnapshot is called.
99+
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
100+
}
101+
}
102+
}
103+
104+
@RequiresApi(Build.VERSION_CODES.O)
105+
private fun captureHardwareBitmap(view: View, reactTag: Int, window: Window) {
106+
val bitmap = createBitmap(view.width, view.height)
107+
val location = IntArray(2)
108+
view.getLocationInWindow(location)
109+
val rect = Rect(location[0], location[1], location[0] + view.width, location[1] + view.height)
110+
// PixelCopy callback is posted to mainHandler, so onBitmapCaptured may run after
111+
// setViewSnapshot has already recorded the target tag for this source tag.
112+
try {
113+
PixelCopy.request(
114+
window,
115+
rect,
116+
bitmap,
117+
{ copyResult ->
118+
if (copyResult == PixelCopy.SUCCESS) {
119+
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
120+
if (hwBitmap != null) {
121+
bitmap.recycle()
122+
onBitmapCaptured(reactTag, hwBitmap)
123+
} else {
124+
onBitmapCaptured(reactTag, bitmap)
125+
}
126+
} else {
127+
bitmap.recycle()
128+
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
129+
}
130+
},
131+
mainHandler,
132+
)
133+
} catch (e: IllegalArgumentException) {
134+
// Window surface may have been destroyed (e.g., device idle/sleep).
135+
// Fall back to software rendering.
136+
bitmap.recycle()
137+
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
138+
}
139+
}
140+
141+
/**
142+
* Maps a previously captured bitmap from a source view to a target pseudo-element view. If the
143+
* bitmap is already available, the snapshot becomes resolved and will be re-applied after mount
144+
* cycles.
145+
*/
146+
fun setViewSnapshot(sourceTag: Int, targetTag: Int) {
147+
UiThreadUtil.runOnUiThread {
148+
pendingTargets[sourceTag] = targetTag
149+
if (sourceTag in viewSnapshots) {
150+
ensureListenerRegistered()
151+
}
152+
}
153+
}
154+
155+
/**
156+
* Clears all snapshots. Called when a view transition ends to release bitmaps and unregister the
157+
* mount listener.
158+
*/
159+
fun clearPendingSnapshots() {
160+
UiThreadUtil.runOnUiThread {
161+
viewSnapshots.clear()
162+
pendingTargets.clear()
163+
if (listenerRegistered) {
164+
listenerRegistered = false
165+
uiManager.removeUIManagerEventListener(this)
166+
}
167+
}
168+
}
169+
170+
override fun willDispatchViewUpdates(uiManager: UIManager) {}
171+
172+
override fun willMountItems(uiManager: UIManager) {}
173+
174+
@UiThread
175+
override fun didMountItems(uiManager: UIManager) {
176+
for ((sourceTag, targetTag) in pendingTargets) {
177+
val smm = mountingManager.getSurfaceManagerForView(targetTag) ?: continue
178+
val bitmap = viewSnapshots[sourceTag] ?: continue
179+
smm.applyViewSnapshot(targetTag, bitmap)
180+
}
181+
}
182+
183+
override fun didDispatchMountItems(uiManager: UIManager) {}
184+
185+
override fun didScheduleMountItems(uiManager: UIManager) {}
186+
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
package com.facebook.react.fabric.mounting
99

1010
import android.annotation.SuppressLint
11+
import android.graphics.Bitmap
1112
import android.os.SystemClock
1213
import android.view.View
1314
import android.view.ViewGroup
1415
import android.view.ViewParent
1516
import androidx.annotation.AnyThread
1617
import androidx.annotation.UiThread
1718
import androidx.collection.SparseArrayCompat
19+
import androidx.core.graphics.drawable.toDrawable
1820
import com.facebook.common.logging.FLog
1921
import com.facebook.infer.annotation.ThreadConfined
2022
import com.facebook.react.bridge.GuardedRunnable
@@ -1089,6 +1091,13 @@ internal constructor(
10891091

10901092
private fun getNullableViewState(reactTag: Int): ViewState? = tagToViewState[reactTag]
10911093

1094+
/** Applies a bitmap as the background of the view with the given tag, if it exists. */
1095+
@UiThread
1096+
public fun applyViewSnapshot(tag: Int, bitmap: Bitmap) {
1097+
val view = getNullableViewState(tag)?.view ?: return
1098+
view.background = bitmap.toDrawable(view.resources)
1099+
}
1100+
10921101
public fun printSurfaceState(): Unit {
10931102
FLog.e(TAG, "Views created for surface $surfaceId:")
10941103
for (viewState in tagToViewState.values) {

packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,30 @@ void FabricMountingManager::synchronouslyUpdateViewOnUIThread(
12291229
synchronouslyUpdateViewOnUIThreadJNI(javaUIManager_, viewTag, propsMap);
12301230
}
12311231

1232+
void FabricMountingManager::captureViewSnapshot(Tag tag, SurfaceId surfaceId) {
1233+
static auto captureViewSnapshotJNI =
1234+
JFabricUIManager::javaClassStatic()->getMethod<void(jint, jint)>(
1235+
"captureViewSnapshot");
1236+
captureViewSnapshotJNI(javaUIManager_, tag, surfaceId);
1237+
}
1238+
1239+
void FabricMountingManager::setViewSnapshot(
1240+
Tag sourceTag,
1241+
Tag targetTag,
1242+
SurfaceId surfaceId) {
1243+
static auto setViewSnapshotJNI =
1244+
JFabricUIManager::javaClassStatic()->getMethod<void(jint, jint, jint)>(
1245+
"setViewSnapshot");
1246+
setViewSnapshotJNI(javaUIManager_, sourceTag, targetTag, surfaceId);
1247+
}
1248+
1249+
void FabricMountingManager::clearPendingSnapshots() {
1250+
static auto clearPendingSnapshotsJNI =
1251+
JFabricUIManager::javaClassStatic()->getMethod<void()>(
1252+
"clearPendingSnapshots");
1253+
clearPendingSnapshotsJNI(javaUIManager_);
1254+
}
1255+
12321256
void FabricMountingManager::scheduleReactRevisionMerge(SurfaceId surfaceId) {
12331257
static const auto scheduleReactRevisionMerge =
12341258
JFabricUIManager::javaClassStatic()->getMethod<void(int32_t)>(

packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ class FabricMountingManager final {
6868

6969
void synchronouslyUpdateViewOnUIThread(Tag viewTag, const folly::dynamic &props);
7070

71+
void captureViewSnapshot(Tag tag, SurfaceId surfaceId);
72+
73+
void setViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId);
74+
75+
void clearPendingSnapshots();
76+
7177
void scheduleReactRevisionMerge(SurfaceId surfaceId);
7278

7379
private:

packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -800,18 +800,24 @@ void FabricUIManagerBinding::schedulerDidUpdateShadowTree(
800800
void FabricUIManagerBinding::schedulerDidCaptureViewSnapshot(
801801
Tag tag,
802802
SurfaceId surfaceId) {
803-
// TODO: implement this
803+
if (mountingManager_) {
804+
mountingManager_->captureViewSnapshot(tag, surfaceId);
805+
}
804806
}
805807

806808
void FabricUIManagerBinding::schedulerDidSetViewSnapshot(
807809
Tag sourceTag,
808810
Tag targetTag,
809811
SurfaceId surfaceId) {
810-
// TODO: implement this
812+
if (mountingManager_) {
813+
mountingManager_->setViewSnapshot(sourceTag, targetTag, surfaceId);
814+
}
811815
}
812816

813817
void FabricUIManagerBinding::schedulerDidClearPendingSnapshots() {
814-
// TODO: implement this
818+
if (mountingManager_) {
819+
mountingManager_->clearPendingSnapshots();
820+
}
815821
}
816822

817823
void FabricUIManagerBinding::onAnimationStarted() {

scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2325,6 +2325,7 @@ class facebook::react::FabricMountingManager {
23252325
public void scheduleReactRevisionMerge(facebook::react::SurfaceId surfaceId);
23262326
public void sendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType);
23272327
public void setIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder);
2328+
public void setViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId);
23282329
public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag viewTag, const folly::dynamic& props);
23292330
public ~FabricMountingManager();
23302331
}

scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2323,6 +2323,7 @@ class facebook::react::FabricMountingManager {
23232323
public void scheduleReactRevisionMerge(facebook::react::SurfaceId surfaceId);
23242324
public void sendAccessibilityEvent(const facebook::react::ShadowView& shadowView, const std::string& eventType);
23252325
public void setIsJSResponder(const facebook::react::ShadowView& shadowView, bool isJSResponder, bool blockNativeResponder);
2326+
public void setViewSnapshot(facebook::react::Tag sourceTag, facebook::react::Tag targetTag, facebook::react::SurfaceId surfaceId);
23262327
public void synchronouslyUpdateViewOnUIThread(facebook::react::Tag viewTag, const folly::dynamic& props);
23272328
public ~FabricMountingManager();
23282329
}

0 commit comments

Comments
 (0)