fix(android): clip TextureView/SurfaceView capture to scroll viewport#656
fix(android): clip TextureView/SurfaceView capture to scroll viewport#656hirvesh 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.
| // 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(); |
| 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()); | ||
| } |
|
@hirvesh Thanks for the PR. |
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.