Repository navigation
Conversation
Preview and PostCapture now hide the status bar; the navigation bar is deliberately kept visible everywhere. Settings, Permissions, any unknown route, and every destination while in multi-window show all bars. Compose's inset APIs are visibility dependent: WindowInsets.statusBars, navigationBars, systemBars and safeDrawing all collapse to zero while the corresponding bar is hidden, so simply hiding a bar shifts the layout. The capture surfaces therefore switch to the *IgnoringVisibility variants, which keep reporting a bar's size regardless of whether it is currently shown, hidden, or transiently revealed. With those in place the layout no longer moves when bar visibility changes, which is what makes the hide/show sequencing across navigation transitions a non-issue. Details: * New SystemBarsPolicy.kt owns bar visibility for the whole app. It maps a navigation route to a policy, applies it idempotently (diffing against the real window state before writing), re-asserts on every resume because the system can reset bars underneath us, and clamps to "show everything" in multi-window. It lives above the NavHost on purpose: during a navigation transition both destinations are composed, so a per-screen effect would let the outgoing screen's cleanup run last and clobber the incoming screen's request. BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE is installed only while the status bar is hidden and reset to BEHAVIOR_DEFAULT otherwise, so it cannot steal edge swipes from scrollable screens. * Status bar icon contrast adapts dynamically: when the status bar is visible on non-capture surfaces (such as Settings), isAppearanceLightStatusBars matches the theme contrast (dark icons on light theme, light icons on dark theme). On capture surfaces, transiently revealed icons remain light over the black viewfinder. * Top real estate utilization: the top indicator controls are housed in a top bar Box sized to accommodate the cutout or status bar height (at least 48dp), occupying the top cutout/status bar band with horizontal cutout insets. This reclaims vertical viewfinder space rather than pushing the controls down into a void below the status bar. * Intentional visual change: enableEdgeToEdge() now uses explicit dark (light content) transparent bar styles. The default "auto" styles pick their light/dark variant from the system-wide configuration rather than the app's own theme, which produced dark, effectively invisible status bar icons over the black viewfinder on a light system. This also drops the translucent scrim "auto" applies below API 29. Navigation bar contrast enforcement is disabled for the same reason. * Bug fix: PostCapture previously applied both the Scaffold's content padding and safeContentPadding(). Scaffold does not consume the insets it reports, so the controls were inset by roughly twice the system bar heights. The Scaffold padding is dropped and the remaining inset excludes gesture insets, which had been pulling the left and right buttons inward. * The Permissions screen had no inset handling at all and drew under both bars; it now applies safeDrawing padding while keeping its background edge to edge. Tests: a plain JUnit table test pins the route-to-policy mapping, including the route patterns with unresolved query-argument placeholders that navigation actually reports. A Robolectric test dispatches synthetic insets and asserts the capture controls keep identical bounds when bars are hidden; it includes a guard case that fails if the environment stops delivering insets into Compose, and both position assertions were confirmed to fail when the modifiers are reverted to the visibility-dependent APIs.
…space - Size the capture top bar from max(displayCutout.top, minInteractiveTouchTarget) instead of statusBarsIgnoringVisibility so the top bar sits in the hidden status bar region rather than reserving an oversized top band. - Add CutoutAwareRow with LookaheadScope and RowScope support so top bar indicators shift horizontally past any intersecting corner or center display cutout while maintaining stable positions during AnimatedVisibility expand/shrink transitions. - Re-assert system bar visibility on window focus gain and call WindowInsetsControllerCompat.hide/show directly to avoid stale root inset visibility checks on activity launch.
Restructures the vertical capture controls so the stack matches the design spec: - Adds a defaulted captureModeCarousel slot to PreviewLayout, rendered in a Box between the shutter row and the lower controls row. The slot height of 32dp is reserved unconditionally, so a future occupant can appear there without shifting the shutter row or any other control. - Gives the shutter row a hard 86dp height (replacing the two 120dp child boxes and the intrinsic-max sizing) and the lower controls row a hard 64dp height, so neither can grow with its content. - Anchors the zoom bar to the 3:4 viewfinder bottom baseline in standard portrait orientation (matching reference camera app behavior). This places the zoom bar cleanly inside the 3:4 preview, keeps it locked at the same physical screen position across aspect ratios (3:4 and 9:16), and adapts dynamically across device screen heights. - Replaces static bottom padding with a navigation-bar-aware clearance (56dp target clearance, 8dp minimum safety margin) that preserves a consistent thumb resting position for the lower controls across both gesture navigation and 3-button navigation. - Extracts every dimension into named constants. - Wraps the stack in BoxWithConstraints so that on short screens the gaps and bottom padding compress while the fixed element heights stay intact. As a result of the new spacing the shutter ring centre sits ~43dp higher than before, matching the design spec. Adds CaptureLayoutPositionTest, a JVM Compose test that renders the layout with an empty and a populated middle slot and asserts the capture button lands at an identical vertical position in both cases, guarding the slot reservation against regressions. The tests pin the window size with Robolectric qualifiers so that both the regular and the compressed spacing are exercised, and assert the concrete expected positions; the short-screen test also asserts the top of the stack is not clipped.
- Introduce CameraSystemBarsEffect as a reusable, drop-in composable in :ui:components:capture to encapsulate edge-to-edge system bar management for camera capture experiences. - Refactor SystemBarsPolicy in :app to delegate to CameraSystemBarsEffect. - Add vertical clearance between the zoom bar and the bottom edge of the 3:4 viewfinder so the zoom pill has comfortable breathing room above the rounded preview boundary.
- Introduce PreviewPortraitDevices multi-preview annotation in :ui:components:capture covering standard phone, compact (<600dp) phone, foldable unfolded portrait, and tablet portrait profiles. - Apply PreviewPortraitDevices to CaptureLayoutPreview and ContentScreenPreview. - Update CaptureLayoutPreview viewfinder placeholder to scale with 3:4 aspect ratio instead of a fixed 600dp height.
Dynamically calculate the available space below the 3:4 viewfinder and scale the control stack gaps and bottom clearance linearly between their minimum and maximum targets. This ensures the shutter row never overlaps the live preview on shorter portrait devices while smoothly expanding to the full design spec spacing on taller screens.
Introduces a pure, framework-independent solver that places a stack of camera control rows and a set of viewfinder frames against a device window, guaranteeing that no viewfinder edge overlaps a control and that the control stack stays put when the user switches aspect ratio. The solver has no Compose or Android dependency and takes no UI state. Control rows are opaque obstacles identified only by id, so it has no knowledge of what any row contains, and content visibility is deliberately not an input: routing that through the solver is how a shutter button ends up shifting under the user's thumb on a mode change. Being framework-free also means the whole evaluation set runs on the JVM in milliseconds and belongs in ordinary presubmit. This commit is the solver and its tests only. Nothing calls it yet. Two deliberate corrections to the algorithm this was ported from: * The compact-toolbar band pinned the stack lift to zero on the premise that compacting the bottom toolbar would absorb the shortfall, but the compacted height is floored and nothing checked whether the band actually offered that much room. When it did not, an overlap was arithmetically guaranteed and the search's only escape route had been disabled. Sweeping 3,780 window geometries found 42 supported shapes that always overlapped, at ordinary phone aspect ratios. Entering the band only when the compacted toolbar fits resolves all 42, and the band is never load-bearing for collision avoidance elsewhere. * The repair search is a cartesian sweep whose last-resort rung swept bottom padding without bounding it by the height of the stack being placed, costing up to 677k candidate evaluations for a single window. The sweep now stops once the stack can no longer be placed, which is exact rather than approximate because that failure mode is monotone in padding, and a hard evaluation budget bounds pathological geometry. Worst-case cost on supported windows is now 1,055 evaluations. Testing: * A golden fixture pins all 66 device, navigation mode and aspect ratio combinations against the reference implementation. Both corrections above were verified to leave every one of those cases byte-identical, so parity and the fixes hold simultaneously. * Property tests sweep 3,780 synthetic window geometries, split by what is actually promised rather than holding everything to one bar. The 1,568 shapes between 16:9 and 21:9 must be collision free; the 2,212 squarer or taller shapes are tablets, foldable inner displays and split-screen panes, which get a different template and need only degrade gracefully. Invariants that hold by construction are still asserted over all 3,780.
…get to 5,000 - Reduce COLLISION_TEST_SLACK from 0.12 dp to float representation tolerance (1e-4f) so the solver enforces the full 4.0 dp minimum clearance rather than stopping one 0.1 dp quantum early on 3-button navigation configurations. - Lower EVALUATION_BUDGET from 20,000 (~25 ms) to 5,000 (~6 ms) to bound worst-case out-of-scope geometry evaluation within a single frame while retaining nearly 5x headroom above the worst in-scope handheld window (1,055 evaluations). - Update golden fixture and property test clearance floor accordingly.
…nputs Replaces hardcoded compact-band and sweep constants in CameraLayoutSolver with geometric formulas computed from CameraLayoutSpec and CameraWindow inputs: - minToolbarHeight derived from minInteractiveTouchTarget - minControlClearance - compactBandMinPadding derived from navigation bar vs. gesture inset comparison - compactBandMin and compactBandMax derived from toolbar heights, clearance, and bottom padding - repair sweep minimum gap read from ControlRow.gapAbove.min
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces a custom camera layout solver (CameraLayoutSolver) along with its default specifications, supporting types, and comprehensive golden and property-based tests to verify layout invariants across various device geometries. The review feedback focuses heavily on performance optimizations in the solver's hot paths—specifically recommending the elimination of intermediate list, iterator, and lambda allocations during candidate evaluation and repair sweeps. Additionally, the feedback notes style guide violations regarding missing @RunWith(JUnit4::class) annotations on the test classes.
temcguir
marked this pull request as draft
September 24, 2026 22:25
- Use rememberUpdatedState(isDarkTheme) in SystemBarsPolicyEffect's cleanup DisposableEffect so toggling dark mode does not transiently restore the status bar. - Annotate SystemBarsPolicyTest with @RunWith(JUnit4::class) and switch assertions to Google Truth. - Simplify cutout inset state reads and guard platform DisplayCutout access with Build.VERSION.SDK_INT >= P in CutoutAwareRow. - Add class-level KDoc to CutoutAwareRowTest and @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") to PostCaptureLayout.
… nav bar icon appearance
…lver and defer UI changes to adaptive-capture-layout
temcguir
changed the base branch from
temcguir/bottom-controls-layout
to
temcguir/edge-to-edge
September 28, 2026 20:50
The reference layout for a 360 x 740 window was outside the solver's feasible set. These changes make it the solution in both gesture and 3-button navigation. - Anchor the bottom row. The preferred padding keeps the toolbar's lower edge at most 54 dp above the window bottom, so a 48 dp navigation bar reduces the padding instead of raising the controls (bottomRowAnchor). Above a navigation bar taller than the gesture zone, the padding floor is minControlClearance. - Let a 9:16 frame that cannot clear the stack from above enclose it, passing behind the fixed top-start control with at least 1 dp to the button's visible top (topStartVisibleInset, topStartMinEdgeGap), before falling back to the top of the window. - Measure the top-weighting cap to the bottom of the window, since the navigation bar reads as black. - Compact the toolbar only when the full-height toolbar does not lay out cleanly. - Bound how far a collision raises the stack. The repair takes the top-ranked candidate no more than INCIDENTAL_LIFT_BUDGET above the lowest collision-free padding the search found. The candidates evaluated are unchanged. - Regenerate the golden fixture (s9, pixel3, and the 3-button cases of pixel9, pixel9_xl, pixel8, pixel7 and pixel_fold) and record the departures from the prototype in its header. - Add window360x740MatchesTheDesignReference, and measure the property test's lower band to the bottom of the window.
The bottom inset of a single device varies: 48 dp for 3-button navigation, 24 dp for gesture navigation from Android 13 (16 dp on Android 10 to 12L), and zero when no bar is reported. The bottom gesture zone varies with it: 32 dp for gesture navigation and the full bar height for 3-button navigation. The solver chose a different layout when either value changed by a few dp, and the tests passed a 24 dp gesture zone that devices do not report. - Make the bottom row anchor two-way. Outside an immersive window the preferred padding is bottomRowAnchor minus the navigation bar height, so a shorter bar, or none, adds the padding that a taller bar removes. An immersive window keeps the designed padding, capped at the anchor. - Treat a navigation bar at least as tall as the gesture zone as covering it, so 3-button navigation (48 dp bar, 48 dp zone) gets the minControlClearance floor. - Default CameraWindow.gestureInset to 32 dp, the gesture navigation value. - Regenerate the gestureInset column of the golden fixture: 32 dp for gesture navigation, 48 dp for 3-button navigation, and 24 dp for the immersive pixel2 and compact_16_9 windows. No solution changed. - Check the 360 x 740 design reference with every reported pair of bottom insets, and add layoutDoesNotMoveWithTheBottomInset. It solves each golden window with every pair and pins the three comparisons it skips because the 9:16 frame changes placement. - Sweep the property test over reported (bar, zone) pairs instead of independent bar heights with a fixed 24 dp zone, and update the measured evaluation counts (worst supported window 938 -> 587).
… in CameraLayoutSolver
temcguir
marked this pull request as ready for review
October 12, 2026 20:30
Kimblebee
requested changes
Oct 15, 2026
- Fix sweepZeroDrop early-exit condition when the 9:16 frame fits below the top bar so tall windows stop once the 3:4 frame is flush. - Add movableFramesFit pruning in RepairSearch so partial row placements that leave no valid position for the 3:4 or 1:1 frame are skipped before evaluating gap combinations. - Reuse a pre-sized FloatArray in minimax, return primitive IntArray from gapGrid, and split evaluate() into placeAll() and materialize() so rejected or non-improving candidates do not allocate layout maps.
The top-start control is always kept at a fixed position below the top bar, so the viewfinder-tracking placement mode is no longer needed. Remove the enum, the spec field and the branches that only applied to that mode. Layouts for the remaining mode are unchanged: the golden fixture and the evaluation-count bounds pass without regeneration.
Kimblebee
approved these changes
Oct 22, 2026
Kimblebee
approved these changes
Oct 22, 2026
- Declare explicit Dp types on the public CameraLayoutDefaults dimensions. - Declare the explicit type of LocalCameraLayoutSolution. - Remove an unused import from CameraLayoutSolver. - Use for loops instead of forEach in the solver tests.
- Replace a not-null assertion with a null-safe check. - Use a List for the gap grids in the generic sweep. - Document and suppress the array lint on the solver's internal primitive-array buffers, which avoid boxing and per-candidate allocations in the search loop.
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.
Summary
CameraLayoutSolver(CameraLayoutSolver.kt,CameraLayoutDefaults.kt,CameraLayoutTypes.kt) in:ui:components:capture, a pure, stateless layout solver that computes vertical coordinates for the camera viewfinder (3:4,1:1,9:16) and the five portrait control rows (TopBar,ZoomBar,CaptureRow,ModeSwitcher,BottomToolbar):CameraWindow).>= 4.0.dpclearance).requiresImmersive) only on compact ~16:9 screens where a 9:16 viewfinder cannot clear a persistent navigation bar.3:4or1:1) with no collision-free position so supported windows do not exhaust the evaluation budget, and stops the zero-drop repair sweep once the primary frame is flush with the top bar.LocalCameraLayoutSolutionfor child components to read the solved geometry.Testing
CameraLayoutSolverGoldenTest&CameraLayoutSolverPropertyTest: Golden and property-based unit test suites covering standard, compact, 3-button navigation, and foldable display profiles, verifying zero bisections, aspect-ratio invariance of control coordinates, and bounded candidate evaluations.