Skip to content

Commit ff6b91c

Browse files
zeyapfacebook-github-bot
authored andcommitted
Fix PixelCopy snapshot for partially off-screen views
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 8117ec6 commit ff6b91c

1 file changed

Lines changed: 37 additions & 13 deletions

File tree

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

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,28 +103,52 @@ 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+
if (clampedRect.isEmpty) {
126+
// Entirely off-screen — nothing to capture.
127+
return
128+
}
129+
130+
val clampedBitmap = createBitmap(clampedRect.width(), clampedRect.height())
131+
// Offset of the clamped region within the full view.
132+
val offsetX = clampedRect.left - viewRect.left
133+
val offsetY = clampedRect.top - viewRect.top
134+
110135
// PixelCopy callback is posted to mainHandler, so onBitmapCaptured may run after
111136
// setViewSnapshot has already recorded the target tag for this source tag.
112137
try {
113138
PixelCopy.request(
114139
window,
115-
rect,
116-
bitmap,
140+
clampedRect,
141+
clampedBitmap,
117142
{ copyResult ->
118143
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-
}
144+
// Compose the clamped capture into a full-size bitmap at the
145+
// correct offset so it aligns with the pseudo-element's bounds.
146+
val fullBitmap = createBitmap(view.width, view.height)
147+
Canvas(fullBitmap).drawBitmap(clampedBitmap, offsetX.toFloat(), offsetY.toFloat(), null)
148+
clampedBitmap.recycle()
149+
onBitmapCaptured(reactTag, fullBitmap)
126150
} else {
127-
bitmap.recycle()
151+
clampedBitmap.recycle()
128152
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
129153
}
130154
},
@@ -133,7 +157,7 @@ internal class ViewTransitionSnapshotManager(
133157
} catch (e: IllegalArgumentException) {
134158
// Window surface may have been destroyed (e.g., device idle/sleep).
135159
// Fall back to software rendering.
136-
bitmap.recycle()
160+
clampedBitmap.recycle()
137161
onBitmapCaptured(reactTag, captureSoftwareBitmap(view))
138162
}
139163
}

0 commit comments

Comments
 (0)