refactor(android): UI-thread-safe view capture with hardened bitmap lifecycle#628
refactor(android): UI-thread-safe view capture with hardened bitmap lifecycle#628
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes incorrect alpha compositing in Android view captures when a translucent parent contains overlapping children, by temporarily forcing translucent ViewGroups to render into software layers during view.draw() and then restoring original layer types.
Changes:
- Add a subtree walk to detect translucent
ViewGroups (0 < alpha < 1) and switch them toLAYER_TYPE_SOFTWAREprior to capture. - Restore original layer types in a
finallyblock to avoid leaving the live UI in a modified state.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
073f508 to
e66871c
Compare
There was a problem hiding this comment.
Pull request overview
Fixes the Android “nested opacity” capture artifact by temporarily forcing translucent ViewGroups to render into software layers during view.draw() capture, then restoring the original layer types afterward.
Changes:
- Adds a subtree walk to switch translucent
ViewGroups withLAYER_TYPE_NONEtoLAYER_TYPE_SOFTWAREbefore capture. - Introduces a
runOnUiThreadBlockinghelper to ensuresetLayerTypemutations occur on the main thread. - Restores original layer types in a
finallyblock after drawing.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
e66871c to
50f740e
Compare
There was a problem hiding this comment.
Pull request overview
This PR targets an Android-only nested-opacity capture artifact by forcing translucent ViewGroup subtrees to be composited as software layers during view.draw(...), then restoring original layer types after capture.
Changes:
- Add a traversal that detects translucent
ViewGroups and temporarily sets them toLAYER_TYPE_SOFTWAREfor capture. - Add a small UI-thread blocking helper to safely mutate view layer types from the capture thread.
- Restore original layer types in a
finallyblock after drawing.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
50f740e to
2540d95
Compare
There was a problem hiding this comment.
Pull request overview
Fixes incorrect alpha compositing for Android view captures when a translucent parent ViewGroup contains overlapping children, by forcing translucent ViewGroups to composite as a single software layer during view.draw() and restoring original layer types afterward.
Changes:
- Add a subtree walk to temporarily switch translucent
ViewGroups fromLAYER_TYPE_NONEtoLAYER_TYPE_SOFTWAREduring capture. - Run layer-type mutations +
view.draw()+ restore as a single UI-thread task with a bounded blocking wait helper.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2540d95 to
781693b
Compare
There was a problem hiding this comment.
Pull request overview
Fixes an Android screenshot rendering issue where a translucent parent ViewGroup with overlapping children is composited incorrectly during capture, causing “nested opacity” artifacts in PNG output.
Changes:
- Temporarily forces certain translucent
ViewGroups toLAYER_TYPE_SOFTWAREduring capture to ensure subtree composition happens before applying parent alpha. - Runs mark → draw → restore as a single UI-thread task with a blocking wait + timeout from the capture executor thread.
- Improves capture failure reporting by including the underlying exception in the Promise rejection.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
781693b to
d6eadc5
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes Android screenshot rendering for nested translucency by temporarily forcing certain translucent ViewGroup subtrees to render as software layers during capture, ensuring correct alpha compositing while preserving view.draw() behavior (e.g., clipping/overflow).
Changes:
- Add UI-thread atomic “mark translucent subtrees → draw → restore” logic to correct nested-opacity blending during capture.
- Introduce a UI-thread blocking helper with timeout/cancellation semantics for safe view mutations during capture.
- Improve snapshot failure reporting (include underlying exception detail) and ensure bitmap recycling happens in a
finally.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
d6eadc5 to
073f673
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses an Android capture rendering correctness issue where nested opacity on ViewGroups with overlapping children is composited incorrectly in software-canvas captures, by temporarily forcing affected subtrees to render as software layers during capture and then restoring the original state.
Changes:
- Add a UI-thread mark/draw/restore flow that switches qualifying translucent
ViewGroups toLAYER_TYPE_SOFTWAREduringview.draw(). - Introduce a blocking UI-thread helper with timeout/cancellation to avoid indefinite hangs.
- Improve snapshot failure rejection by including exception details and the throwable.
Comments suppressed due to low confidence (1)
android/src/main/java/fr/greweb/reactnativeviewshot/ViewShot.java:490
- The body of this try block isn’t indented, which makes the new capture logic harder to read and review. Please reformat/indent the code inside the try so it matches the rest of this file’s style.
try {
final Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
073f673 to
d8116e8
Compare
There was a problem hiding this comment.
Pull request overview
Adjusts Android view capture to fix nested-opacity blending issues by forcing certain translucent ViewGroup subtrees to render via software layers during snapshot capture, while adding UI-thread blocking/timeout safeguards.
Changes:
- Adds subtree traversal to temporarily set qualifying translucent
ViewGroups toLAYER_TYPE_SOFTWAREduringview.draw(), then restores layer types. - Introduces a
runOnUiThreadBlocking()helper with a hard timeout to avoid indefinite hangs when posting capture work to the main thread. - Improves snapshot failure reporting by including exception details and passing the throwable to
promise.reject.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I'll need to manually test it |
…ifecycle Move setLayerType + view.draw() onto the UI thread, with a hard timeout, cancellation, and CAS-based bitmap pool handoff so canvas-backing bitmaps can't be recycled mid-draw. No visible behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
247542e to
f8069c4
Compare
Summary
This PR started as an attempt to fix the Android nested-opacity capture artifact (see #616 discussion) by forcing translucent
ViewGroups toLAYER_TYPE_SOFTWAREduring capture. In practice that did not produce a visible fix — the captured output still shows the same nested-opacity blending as master. So this PR does not ship the rendering correctness fix it set out to.What it does ship is a meaningful hardening of the Android capture path. No visible regression, no behavior change for users on the success path, but the failure modes around UI-thread access, timeouts, and bitmap recycling are now well-defined. Worth shipping as a standalone improvement; the nested-opacity work continues elsewhere.
What's actually in this PR
UI-thread handoff (
runOnUiThreadBlocking,CancellableUiTask)view.draw()now runs on the UI thread instead of the capture executor — this is the thread that owns the view tree, sosetLayerType/getLayerType/ draw traversal are no longer crossing thread boundaries.Handler(mainLooper)and waits viaCountDownLatch. Already-on-UI-thread fast path, interrupt-safe wait (loops until counted down, re-interrupts on exit).UiThreadBlockTimeoutException, sized to match the existingSURFACE_VIEW_READ_PIXELS_TIMEOUT) so a stuck main thread can't hang the capture forever.Cancellation + bitmap lifecycle hardening
STATE_QUEUED/RUNNING/DONE) with CAS between caller (timeout) and runnable (entry) so exactly one side owns cleanup. On timeout the caller callsremoveCallbacks, flips acancelledflag, and CAS-promotes still-queued tasks toDONE.AtomicReference<Bitmap>+getAndSet(null)— prevents the canvas-backing bitmap from being returned to the pool whileview.draw()may still be drawing into it under timeout races.handler.post()return value checked; failure path signalsuiTaskFinishedso the caller'sfinallycan recycle immediately.Throwablethrown inside the UI runnable is captured and rethrown on the caller thread, so it surfaces aspromise.reject(...)instead of crashing the UI thread.Layer-marking attempt (kept but inert in practice)
markSubtreeAlphaLayerswalks the tree and forcesLAYER_TYPE_SOFTWAREon translucentViewGroups withchildCount > 1,hasOverlappingRendering(), andLAYER_TYPE_NONE; restored infinally.Misc
executeImplreject message falls back toex.toString()whengetMessage()is null.Diff: 1 file, +325/-79 lines.
Test plan
🤖 Generated with Claude Code