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
18 changes: 18 additions & 0 deletions packages/react-native/React/Fabric/RCTScheduler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ void schedulerDidUpdateShadowTree(const std::unordered_map<Tag, folly::dynamic>
// This delegate method is not currently used on iOS.
}

void schedulerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override
{
// Does nothing.
// View transition snapshots are not currently implemented on iOS.
}

void schedulerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) override
{
// Does nothing.
// View transition snapshots are not currently implemented on iOS.
}

void schedulerDidClearPendingSnapshots() override
{
// Does nothing.
// View transition snapshots are not currently implemented on iOS.
}

private:
void *scheduler_;
};
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -2275,6 +2275,7 @@ public final class com/facebook/react/fabric/FabricUIManagerProviderImpl : com/f

public final class com/facebook/react/fabric/mounting/SurfaceMountingManager {
public final fun addViewAt (III)V
public final fun applyViewSnapshot (ILandroid/graphics/Bitmap;)V
public final fun attachRootView (Landroid/view/View;Lcom/facebook/react/uimanager/ThemedReactContext;)V
public final fun deleteView (I)V
public final fun enqueuePendingEvent (ILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ public class FabricUIManager

private boolean mDriveCxxAnimations = false;

private @Nullable ViewTransitionSnapshotManager mViewTransitionSnapshotManager;

private long mDispatchViewUpdatesTime = 0l;
private long mCommitStartTime = 0l;
private long mLayoutTime = 0l;
Expand Down Expand Up @@ -811,6 +813,40 @@ public void synchronouslyUpdateViewOnUIThread(final int reactTag, final Readable
ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber);
}

/** Called from C++ via JNI. */
@SuppressLint("NotInvokedPrivateMethod")
@SuppressWarnings("unused")
@AnyThread
@ThreadConfined(ANY)
private void captureViewSnapshot(final int reactTag, final int surfaceId) {
getViewTransitionSnapshotManager().captureViewSnapshot(reactTag, surfaceId);
}

/** Called from C++ via JNI. */
@SuppressLint("NotInvokedPrivateMethod")
@SuppressWarnings("unused")
@AnyThread
@ThreadConfined(ANY)
private void setViewSnapshot(final int sourceTag, final int targetTag, final int surfaceId) {
getViewTransitionSnapshotManager().setViewSnapshot(sourceTag, targetTag);
}

/** Called from C++ via JNI. */
@SuppressLint("NotInvokedPrivateMethod")
@SuppressWarnings("unused")
@AnyThread
@ThreadConfined(ANY)
private void clearPendingSnapshots() {
getViewTransitionSnapshotManager().clearPendingSnapshots();
}

private synchronized ViewTransitionSnapshotManager getViewTransitionSnapshotManager() {
if (mViewTransitionSnapshotManager == null) {
mViewTransitionSnapshotManager = new ViewTransitionSnapshotManager(this, mMountingManager);
}
return mViewTransitionSnapshotManager;
}

@SuppressLint("NotInvokedPrivateMethod")
@SuppressWarnings("unused")
@AnyThread
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* 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.fabric

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.PixelCopy
import android.view.View
import android.view.Window
import androidx.annotation.RequiresApi
import androidx.annotation.UiThread
import androidx.core.graphics.createBitmap
import com.facebook.infer.annotation.ThreadConfined
import com.facebook.react.bridge.UIManager
import com.facebook.react.bridge.UIManagerListener
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.fabric.mounting.MountingManager

/**
* Manages bitmap snapshots of views during view transitions. Captures bitmaps from old views and
* applies them to pseudo-element shadow nodes, re-applying after each mount cycle since views may
* be recreated. Cleans up entries whose views have been deleted.
*/
@OptIn(UnstableReactNativeAPI::class)
internal class ViewTransitionSnapshotManager(
private val uiManager: FabricUIManager,
private val mountingManager: MountingManager,
) : UIManagerListener {

companion object {
private fun captureSoftwareBitmap(view: View): Bitmap {
val bitmap = createBitmap(view.width, view.height)
view.draw(Canvas(bitmap))
return bitmap
}
}

// Captured bitmaps keyed by source tag. Populated by onBitmapCaptured.
@ThreadConfined(ThreadConfined.UI) private val viewSnapshots = LinkedHashMap<Int, Bitmap>()

// Source→target tag mapping. Populated by setViewSnapshot.
// A snapshot is resolved when both maps contain an entry for the same source tag.
@ThreadConfined(ThreadConfined.UI) private val pendingTargets = LinkedHashMap<Int, Int>()

@ThreadConfined(ThreadConfined.UI) private var listenerRegistered = false

private val mainHandler = Handler(Looper.getMainLooper())

@UiThread
private fun onBitmapCaptured(reactTag: Int, bitmap: Bitmap) {
viewSnapshots[reactTag] = bitmap
if (reactTag in pendingTargets) {
ensureListenerRegistered()
}
}

@UiThread
private fun ensureListenerRegistered() {
if (!listenerRegistered) {
listenerRegistered = true
uiManager.addUIManagerEventListener(this)
}
}

/**
* Captures a bitmap snapshot of the view identified by the given tag. On API 26+, uses PixelCopy
* to capture directly from the GPU-composited surface (faster for complex views, captures
* hardware-accelerated content). Falls back to View.draw() on older APIs.
*/
fun captureViewSnapshot(reactTag: Int, surfaceId: Int) {
UiThreadUtil.runOnUiThread {
val smm = mountingManager.getSurfaceManager(surfaceId) ?: return@runOnUiThread
if (!smm.getViewExists(reactTag)) return@runOnUiThread
val view = smm.getView(reactTag)
if (view.width <= 0 || view.height <= 0) return@runOnUiThread

val window =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
(view.context as? com.facebook.react.bridge.ReactContext)?.getCurrentActivity()?.window
} else {
null
}

if (window != null) {
captureHardwareBitmap(view, reactTag, window)
} else {
// Software fallback runs synchronously, so onBitmapCaptured always
// completes before setViewSnapshot is called.
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
private fun captureHardwareBitmap(view: View, reactTag: Int, window: Window) {
val bitmap = createBitmap(view.width, view.height)
val location = IntArray(2)
view.getLocationInWindow(location)
val rect = Rect(location[0], location[1], location[0] + view.width, location[1] + view.height)
// PixelCopy callback is posted to mainHandler, so onBitmapCaptured may run after
// setViewSnapshot has already recorded the target tag for this source tag.
try {
PixelCopy.request(
window,
rect,
bitmap,
{ copyResult ->
if (copyResult == PixelCopy.SUCCESS) {
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
if (hwBitmap != null) {
bitmap.recycle()
onBitmapCaptured(reactTag, hwBitmap)
} else {
onBitmapCaptured(reactTag, bitmap)
}
} else {
bitmap.recycle()
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
}
},
mainHandler,
)
} catch (e: IllegalArgumentException) {
// Window surface may have been destroyed (e.g., device idle/sleep).
// Fall back to software rendering.
bitmap.recycle()
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
}
}

/**
* Maps a previously captured bitmap from a source view to a target pseudo-element view. If the
* bitmap is already available, the snapshot becomes resolved and will be re-applied after mount
* cycles.
*/
fun setViewSnapshot(sourceTag: Int, targetTag: Int) {
UiThreadUtil.runOnUiThread {
pendingTargets[sourceTag] = targetTag
if (sourceTag in viewSnapshots) {
ensureListenerRegistered()
}
}
}

/**
* Clears all snapshots. Called when a view transition ends to release bitmaps and unregister the
* mount listener.
*/
fun clearPendingSnapshots() {
UiThreadUtil.runOnUiThread {
viewSnapshots.clear()
pendingTargets.clear()
if (listenerRegistered) {
listenerRegistered = false
uiManager.removeUIManagerEventListener(this)
}
}
}

override fun willDispatchViewUpdates(uiManager: UIManager) {}

override fun willMountItems(uiManager: UIManager) {}

@UiThread
override fun didMountItems(uiManager: UIManager) {
for ((sourceTag, targetTag) in pendingTargets) {
val smm = mountingManager.getSurfaceManagerForView(targetTag) ?: continue
val bitmap = viewSnapshots[sourceTag] ?: continue
smm.applyViewSnapshot(targetTag, bitmap)
}
}

override fun didDispatchMountItems(uiManager: UIManager) {}

override fun didScheduleMountItems(uiManager: UIManager) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
package com.facebook.react.fabric.mounting

import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.SystemClock
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import androidx.annotation.AnyThread
import androidx.annotation.UiThread
import androidx.collection.SparseArrayCompat
import androidx.core.graphics.drawable.toDrawable
import com.facebook.common.logging.FLog
import com.facebook.infer.annotation.ThreadConfined
import com.facebook.react.bridge.GuardedRunnable
Expand Down Expand Up @@ -1089,6 +1091,13 @@ internal constructor(

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

/** Applies a bitmap as the background of the view with the given tag, if it exists. */
@UiThread
public fun applyViewSnapshot(tag: Int, bitmap: Bitmap) {
val view = getNullableViewState(tag)?.view ?: return
view.background = bitmap.toDrawable(view.resources)
}

public fun printSurfaceState(): Unit {
FLog.e(TAG, "Views created for surface $surfaceId:")
for (viewState in tagToViewState.values) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,30 @@ void FabricMountingManager::synchronouslyUpdateViewOnUIThread(
synchronouslyUpdateViewOnUIThreadJNI(javaUIManager_, viewTag, propsMap);
}

void FabricMountingManager::captureViewSnapshot(Tag tag, SurfaceId surfaceId) {
static auto captureViewSnapshotJNI =
JFabricUIManager::javaClassStatic()->getMethod<void(jint, jint)>(
"captureViewSnapshot");
captureViewSnapshotJNI(javaUIManager_, tag, surfaceId);
}

void FabricMountingManager::setViewSnapshot(
Tag sourceTag,
Tag targetTag,
SurfaceId surfaceId) {
static auto setViewSnapshotJNI =
JFabricUIManager::javaClassStatic()->getMethod<void(jint, jint, jint)>(
"setViewSnapshot");
setViewSnapshotJNI(javaUIManager_, sourceTag, targetTag, surfaceId);
}

void FabricMountingManager::clearPendingSnapshots() {
static auto clearPendingSnapshotsJNI =
JFabricUIManager::javaClassStatic()->getMethod<void()>(
"clearPendingSnapshots");
clearPendingSnapshotsJNI(javaUIManager_);
}

void FabricMountingManager::scheduleReactRevisionMerge(SurfaceId surfaceId) {
static const auto scheduleReactRevisionMerge =
JFabricUIManager::javaClassStatic()->getMethod<void(int32_t)>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class FabricMountingManager final {

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

void captureViewSnapshot(Tag tag, SurfaceId surfaceId);

void setViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId);

void clearPendingSnapshots();

void scheduleReactRevisionMerge(SurfaceId surfaceId);

private:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,29 @@ void FabricUIManagerBinding::schedulerDidUpdateShadowTree(
// no-op
}

void FabricUIManagerBinding::schedulerDidCaptureViewSnapshot(
Tag tag,
SurfaceId surfaceId) {
if (mountingManager_) {
mountingManager_->captureViewSnapshot(tag, surfaceId);
}
}

void FabricUIManagerBinding::schedulerDidSetViewSnapshot(
Tag sourceTag,
Tag targetTag,
SurfaceId surfaceId) {
if (mountingManager_) {
mountingManager_->setViewSnapshot(sourceTag, targetTag, surfaceId);
}
}

void FabricUIManagerBinding::schedulerDidClearPendingSnapshots() {
if (mountingManager_) {
mountingManager_->clearPendingSnapshots();
}
}

void FabricUIManagerBinding::onAnimationStarted() {
auto mountingManager = getMountingManager("onAnimationStarted");
if (!mountingManager) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ class FabricUIManagerBinding : public jni::HybridClass<FabricUIManagerBinding>,

void schedulerDidUpdateShadowTree(const std::unordered_map<Tag, folly::dynamic> &tagToProps) override;

void schedulerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override;

void schedulerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) override;

void schedulerDidClearPendingSnapshots() override;

void setPixelDensity(float pointScaleFactor);

void driveCxxAnimations();
Expand Down
Loading
Loading