fix(android): clip TextureView/SurfaceView capture to scroll viewport#656
Open
hirvesh wants to merge 1 commit into
Open
fix(android): clip TextureView/SurfaceView capture to scroll viewport#656hirvesh wants to merge 1 commit into
hirvesh wants to merge 1 commit into
Conversation
On Android, captures composite each TextureView/SurfaceView surface separately from the software view.draw() pass. That blit applied the ancestor transform chain but no ancestor clip and no scroll offset, so a surface larger than its scroll container (e.g. a Skia/GL canvas inside a horizontal ScrollView) bled past the viewport to the output bitmap edge, and scrolled content was captured from offset 0. - Clip each TextureView/SurfaceView blit to the on-screen frame of its scrolling ancestors (ScrollView / HorizontalScrollView). - Subtract each parent's scroll in applyTransformations so the captured window matches what is visible on screen. iOS is unaffected: drawViewHierarchyInRect already honors the scroll view's clip.
There was a problem hiding this comment.
Pull request overview
This PR updates the Android capture pipeline to better match on-screen rendering when TextureView/SurfaceView content is embedded inside scrolled containers, preventing oversized surfaces from bleeding outside the visible scroll viewport and aligning captures with the current scroll position.
Changes:
- Clip
TextureView/SurfaceViewoverlay blits to the viewport of scrolling ancestors (ScrollView/HorizontalScrollView). - Update the transformation walk to subtract each parent’s
scrollX/scrollYso overlay positioning matches what’s visible on screen. - Add helpers to compute ancestor offsets in the capture-root coordinate space.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+901
to
+912
| // A child is drawn by Android at (getLeft - parent.scrollX), but this | ||
| // walk previously used getLeft only, so content inside a scrolled | ||
| // container was positioned at scroll offset 0. Track the parent of each | ||
| // view and subtract its scroll so the captured window matches what is | ||
| // actually visible on screen. | ||
| View parent = root; | ||
| for (final View v : ms) { | ||
| c.save(); | ||
|
|
||
| // apply each view transformations, so each child will be affected by them | ||
| final float dx = v.getLeft() + ((v != child) ? v.getPaddingLeft() : 0) + v.getTranslationX(); | ||
| final float dy = v.getTop() + ((v != child) ? v.getPaddingTop() : 0) + v.getTranslationY(); | ||
| final float dx = v.getLeft() - parent.getScrollX() + ((v != child) ? v.getPaddingLeft() : 0) + v.getTranslationX(); | ||
| final float dy = v.getTop() - parent.getScrollY() + ((v != child) ? v.getPaddingTop() : 0) + v.getTranslationY(); |
Comment on lines
+948
to
+952
| if (parent == root) break; | ||
| if (parent instanceof HorizontalScrollView || parent instanceof ScrollView) { | ||
| final float[] off = offsetInRoot(root, parent); | ||
| c.clipRect(off[0], off[1], off[0] + parent.getWidth(), off[1] + parent.getHeight()); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On Android, captures composite each
TextureView/SurfaceViewsurface separately from the softwareview.draw()pass (those views render blank in software). That second blit applies the ancestor transform chain (applyTransformations) but no ancestor clip and no scroll offset.So when a surface is larger than its scroll container — e.g. a wide
@shopify/react-native-skia<Canvas>(or a GL view) inside a horizontalScrollView— its full surface is drawn, bounded only by the output bitmap edge. It bleeds past the visible viewport, overrunning sibling padding / rounded corners. Scrolled content is also captured from offset 0 rather than the visible window.iOS is unaffected —
drawViewHierarchyInRect:already renders through the scroll view'sclipsToBounds.Fix
TextureView/SurfaceViewblit to the on-screen frame of its scrolling ancestors (ScrollView/HorizontalScrollView) before drawing.scrollX/scrollYinapplyTransformationsso the captured window matches what's visible.Notes / limitations
ScrollViewtrick (doublescaleX:-1), where content lands back in the untransformed frame.ScrollView) on a physical Pixel 9a, new architecture. Happy to add an example screen + Android reference snapshot to the Detox suite if you'd like — wanted to confirm the approach first.