Repository navigation
Composite multi-window screenshots into a single image - #101
Open
alecramirez wants to merge 1 commit into
Open
alecramirez wants to merge 1 commit into
alecramirez wants to merge 1 commit into
Conversation
alecramirez
force-pushed
the
alramirez/composite-multi-window-screenshot
branch
12 times, most recently
from
September 19, 2026 01:44
62af441 to
bcdc73b
Compare
When Shaky is triggered while a dialog, bottom sheet or Compose popup is on screen, the PixelCopy fallback captures each window as its own bitmap and CollectDataTask writes them out as separate files: the first becomes the screenshot, the rest become attachments. A reporter who was looking at one screen therefore files a report carrying several partial images, none of which shows what they actually saw. Add an opt-in ShakeDelegate#enableMultiWindowCompositing() that flattens those per-window bitmaps back into one screenshot. The windows are drawn back-to-front at their on-screen positions onto a bitmap sized to their union, and the dim a window casts behind itself is recreated from its FLAG_DIM_BEHIND/dimAmount layout params, since the window manager draws that dim outside of any window's surface and PixelCopy never sees it. The flag is off by default and only applies on top of the existing enableMultiWindowCapture(), so the per-window behavior is unchanged for everyone who does not opt in. Compositing is a much larger allocation than any single window, so a failure to composite recycles the captured windows and reports failure, letting Shaky degrade to its Canvas fallback rather than crash the host app. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
alecramirez
force-pushed
the
alramirez/composite-multi-window-screenshot
branch
from
September 19, 2026 02:08
bcdc73b to
c717fd0
Compare
alecramirez
marked this pull request as ready for review
September 19, 2026 02:28
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
When Shaky is triggered while a dialog or bottom sheet is on screen, the PixelCopy fallback path captures each window as its own bitmap, and
CollectDataTaskwrites them out as separate files. A reporter who was looking at one screen ends up filing several partial images — the activity with a blank hole where the sheet was, and the sheet alone on transparency — none of which shows what they actually saw.Change
Add an opt-in
ShakeDelegate#enableMultiWindowCompositing()that flattens those per-window bitmaps into a single screenshot.The windows are drawn back-to-front at their on-screen positions onto a bitmap sized to their union. Each window above the base layer is preceded by the dim it casts behind itself, recreated from its
FLAG_DIM_BEHIND/dimAmount, because the window manager draws that dim outside of any window's surface — PixelCopy never captures it, so the composite would otherwise look flat.Copying only the window's region of its surface
A window reserves a margin around its frame for what it draws outside it, notably the drop shadow under a dialog or bottom sheet, so its surface is larger than the window.
PixelCopy.request(Surface, Bitmap, …)copies the whole surface and scales it to fit the destination bitmap, which is sized to the root view — so a shadowed window comes back shrunk toward the centre of its own bitmap. Measured on a bottom sheet: a 1440x2870 window with a 1536x2966 surface, i.e. a 48px margin per side, arriving scaled to 0.937.Compositing therefore passes a source rect, derived from
View#getLocationInSurface, so only the window's own region is copied and the result stays 1:1. This is what the platform's ownPixelCopy.request(Window, …)does internally; that overload isn't usable here because a dialog'sWindowisn't reachable from the root views this class enumerates.The per-window path deliberately keeps copying the whole surface, so its output is unchanged.
Notes
enableMultiWindowCapture(), so per-window behavior is unchanged unless you opt in. The Falcon path is untouched.captureMultipleAsync()is now a thin adapter over a new privatecaptureAllWindows(), which also hands back theViewRootDatathat compositing needs for geometry. Behavior is equivalent.captureWithCanvas()fallback instead of crashing the host app.Testing
./gradlew :shaky:checkpasses (lint + unit tests).Verified on device against a bottom sheet, comparing the composite to a reference screenshot of the same screen. Calibrating the transform on the activity content alone and using it to predict where the sheet should land: predicted panel top 1128px, measured 1128px; panel spans 0.0000–0.9982 of the width against the reference's 0.0000–0.9985; and the sheet's internal content scale is 0.7901 against the activity's 0.790.