Skip to content

Commit ce54d82

Browse files
zeyapfacebook-github-bot
authored andcommitted
Fix PixelCopy snapshot for partially off-screen views (facebook#56608)
Summary: [Internal] [Fixed] - Fix PixelCopy snapshot for partially off-screen PixelCopy captures only the visible portion of the window surface. When a view is partially off-screen, the capture rect extends beyond the window bounds, resulting in a bitmap where only the visible region has content. This partial bitmap then gets stretched to fill the full-size pseudo-element, causing visual distortion. Fix by clamping the PixelCopy rect to the window bounds and compositing the clamped capture into a full-size bitmap at the correct offset. The off-screen portions remain transparent instead of being stretched. If the view is entirely off-screen, skip capture entirely — the pseudo-element will have no snapshot applied (didMountItems skips tags without a captured bitmap). Differential Revision: D102360642
1 parent ca53608 commit ce54d82

1 file changed

Lines changed: 39 additions & 13 deletions

File tree

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

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,28 +103,54 @@ internal class ViewTransitionSnapshotManager(
103103

104104
@RequiresApi(Build.VERSION_CODES.O)
105105
private fun captureHardwareBitmap(view: View, reactTag: Int, window: Window) {
106-
val bitmap = createBitmap(view.width, view.height)
107106
val location = IntArray(2)
108107
view.getLocationInWindow(location)
109-
val rect = Rect(location[0], location[1], location[0] + view.width, location[1] + view.height)
108+
109+
// The view's rect in window coordinates.
110+
val viewRect =
111+
Rect(location[0], location[1], location[0] + view.width, location[1] + view.height)
112+
113+
// Clamp to window bounds — PixelCopy only captures what's visible on the
114+
// window surface. Without clamping, off-screen portions are black/empty
115+
// and the partial result gets stretched to fill the pseudo-element.
116+
val windowWidth = window.decorView.width
117+
val windowHeight = window.decorView.height
118+
val clampedRect =
119+
Rect(
120+
viewRect.left.coerceAtLeast(0),
121+
viewRect.top.coerceAtLeast(0),
122+
viewRect.right.coerceAtMost(windowWidth),
123+
viewRect.bottom.coerceAtMost(windowHeight),
124+
)
125+
126+
if (clampedRect.isEmpty) {
127+
// Entirely off-screen — nothing to capture.
128+
return
129+
}
130+
131+
val clampedBitmap = createBitmap(clampedRect.width(), clampedRect.height())
132+
// Offset of the clamped region within the full view.
133+
val offsetX = clampedRect.left - viewRect.left
134+
val offsetY = clampedRect.top - viewRect.top
135+
110136
// PixelCopy callback is posted to mainHandler, so onBitmapCaptured may run after
111137
// setViewSnapshot has already recorded the target tag for this source tag.
112138
try {
113139
PixelCopy.request(
114140
window,
115-
rect,
116-
bitmap,
141+
clampedRect,
142+
clampedBitmap,
117143
{ copyResult ->
118144
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-
}
145+
// Compose the clamped capture into a full-size bitmap at the
146+
// correct offset so it aligns with the pseudo-element's bounds.
147+
val fullBitmap = createBitmap(view.width, view.height)
148+
Canvas(fullBitmap)
149+
.drawBitmap(clampedBitmap, offsetX.toFloat(), offsetY.toFloat(), null)
150+
clampedBitmap.recycle()
151+
onBitmapCaptured(reactTag, fullBitmap)
126152
} else {
127-
bitmap.recycle()
153+
clampedBitmap.recycle()
128154
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
129155
}
130156
},
@@ -133,7 +159,7 @@ internal class ViewTransitionSnapshotManager(
133159
} catch (e: IllegalArgumentException) {
134160
// Window surface may have been destroyed (e.g., device idle/sleep).
135161
// Fall back to software rendering.
136-
bitmap.recycle()
162+
clampedBitmap.recycle()
137163
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
138164
}
139165
}

0 commit comments

Comments
 (0)