Skip to content

add 7-mode CameraLayoutMode orchestrator and wire adaptive CaptureScaffold slots - #653

Draft
Kimblebee wants to merge 5 commits into
kim/adaptive/layoutsfrom
kim/adaptive/layout-orchestrator
Draft

Kimblebee wants to merge 5 commits into
kim/adaptive/layoutsfrom
kim/adaptive/layout-orchestrator

Conversation

@Kimblebee

@Kimblebee Kimblebee commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked PR: Based on kim/adaptive/layouts

Summary

Introduces the 7-mode CameraLayoutMode classifier with 14.dp (±7.dp) hysteresis deadbands and turns CaptureScaffoldLayout into a live movableContentOf-backed adaptive orchestrator dispatching across all 5 posture layouts introduced in kim/adaptive/layouts. Wires real CaptureScaffold UI controls across compact, foldable, tablet, and compact-short postures via CaptureSlotScope.orientation and isCompact, isolating temporary vertical placeholders strictly to TopBar (#649) and CaptureModeToggle (#635).

Key Changes

  • CameraLayoutMode.kt (new):
    • Adds the 7-mode CameraLayoutMode enum (COMPACT, COMPACT_SLIM, COMPACT_SHORT, COMPACT_SHORT_SLIM, FOLDABLE, TABLET_PORTRAIT, TABLET_LANDSCAPE), LocalCameraLayoutMode, classifyCameraLayoutMode, isFullScreenCompactPhoneWindow, rememberIsFullScreenCompactPhone, and rememberCameraLayoutMode.
  • CaptureLayout.kt:
    • Converts CaptureScaffoldLayout into the 7-mode BoxWithConstraints orchestrator, wrapping viewfinder in movableContentOf so the live preview surface reparents across posture transitions without disposal.
    • Maps COMPACT / COMPACT_SLIM → CompactPortraitLayout (with includeStatusBarsInTopBar = !isFullScreenCompactPhone), COMPACT_SHORT / COMPACT_SHORT_SLIM → CompactShortLayout, FOLDABLE → FoldableLayout, TABLET_PORTRAIT → TabletPortraitLayout, and TABLET_LANDSCAPE → TabletLandscapeLayout.
    • Adds 9 @Preview functions covering all 7 CameraLayoutMode postures across portrait and horizontal orientations.
  • CaptureScaffold.kt & CompactShortLayout.kt:
    • Forwards CaptureSlotScope.orientation and isCompact into CaptureScaffoldDefaults.ZoomControls (enabling real vertical ZoomBar in StartSideRail), CaptureScaffoldDefaults.TopBar (vertical placeholder for #649), and CaptureScaffoldDefaults.CaptureModeToggle (interactive vertical Role.Switch placeholder for #635 with mode switching and disabled-rationale snackbars), and passes isErrorDialogShowing explicitly to CaptureScaffoldLayout.
    • Updates CaptureScaffoldDefaults.QuickSettings to render QuickSettingsScaffoldContent on CompactPortraitLayout and QuickSettingsContent (without bottom-sheet nav-bar insets) inside floating Quick Settings cards on side-rail layouts, and uses placeRelative inside CompactShortLayout's ConstrainedSlot.
  • SideRailCaptureLayouts.kt:
    • Applies symmetric horizontal topOverlayClearance padding to the bottom-center captureModeCarousel slot when config.clearRailsForQuickSettings is true so full-width carousels never overlap StartSideRail or EndSideRail.

Technical Notes

  • 14.dp (±7.dp) Hysteresis Deadbands & SideEffect History: classifyCameraLayoutMode biases every threshold crossing (360.dp slim, 480.dp short, 600.dp wide, 480.dp wide-height, 960.dp / 1.45f foldable-vs-tablet, and 1:1 square tablet diagonal) by ±7.dp around previousMode. rememberCameraLayoutMode stores previousMode in a plain holder mutated inside SideEffect so committed transitions never trigger a redundant second recomposition and aborted compositions never corrupt hysteresis state.
  • Rotation-Stable Foldable vs. Tablet Classification: FOLDABLE requires widthDp >= 600.dp, heightDp >= 480.dp, maxDim < 960.dp, and maxDim < minDim × 1.45f. Using minDim/maxDim keeps squarish unfolded inner displays (673×841dp to 852×883dp) in FoldableLayout (0.50f rails) across 90° rotation, while routing tall compact tablets (600×900dp, ratio 1.50) to TabletPortraitLayout (0.70f lower thumb zone) and large 4:3 tablets (1024×768dp, maxDim >= 960.dp) to tablet layouts.
  • Full-Screen Compact Phone Guard: rememberIsFullScreenCompactPhone observes LocalConfiguration and ComponentActivity.addOnMultiWindowModeChangedListener (sw in 1 until 600, non-multi-window, maxDim >= 480.dp, container within 2.dp of screen bounds) so rotating a wide compact phone never jumps into TabletLandscapeLayout, while 360×360dp square cover screens (maxDim < 480.dp) still route to CompactShortLayout.
  • movableContentOf Across Subcomposition: Wrapping viewfinder in movableContentOf preserves its slot table and DisposableEffect lifecycle (composeCount == 1, disposeCount == 0) even when moving between CompactPortraitLayout (BottomSheetScaffold SubcomposeLayout), CompactShortLayout (BoxWithConstraints), and standard Box side-rail layouts.

Pre-Existing Bugs Fixed

  • Multi-window compact top-bar status-bar overlap (CaptureLayout.kt): Replaced hardcoded includeStatusBarsInTopBar = false from kim/adaptive/layouts with !isFullScreenCompactPhone so visible multi-window status bars on narrow compact panes (480×800dp) no longer overlap the top status indicator row.
  • Side-rail CaptureModeCarousel corner overlap (SideRailCaptureLayouts.kt): Added horizontal topOverlayClearance padding to slots.captureModeCarousel on rail-cleared postures (FoldableLayout, TabletPortraitLayout, TabletLandscapeLayout) while leaving CompactShortLayout (clearRailsForQuickSettings = false) singly constrained by withCondensedSizeCaps.
  • Floating Quick Settings card double nav-bar inset (CaptureScaffold.kt): Gated QuickSettingsScaffoldContent (WindowInsets.navigationBarsIgnoringVisibility) on LocalActiveCaptureLayout.current == "CompactPortraitLayout", rendering QuickSettingsContent directly inside side-rail floating cards.

Not Included Yet

  • Comes later in this series:
    • Vertical TopBar (#649), Vertical CaptureModeToggle (#635), & QuickSettingsOverlayCard (#648): Vertical side-rail slots temporarily render PlaceholderControlBox stand-ins (with full toggle & snackbar behavior on CaptureModeToggle), and side-rail Quick Settings renders real QuickSettingsContent inside QuickSettingsOverlayCardPlaceholder until #648 merges.
    • Counter-rotation for wide compact windows (kim/adaptive/layout-rotation): Anchors the portrait control stack to the physical side edge, hosts the viewfinder in an un-rotated OuterViewfinderHost so SurfaceView and 10-bit HDR stay active, and adds the layout mode to the debug readout.
    • CameraX window-rotation sync (kim/adaptive/viewfinder-rotation): Updates Preview.targetRotation in place via TransientSessionSettings.windowRotation whenever the host window rotates.
    • Upright icons and labels (kim/adaptive/content-gravity): Handles per-component rotation. Tooltip (#622) and zoom bar RTL polish (#644) remain independent.
  • Open follow-ups:
    • Layouts without a UX spec: Foldable and tablet layouts follow official design specs. Four window shapes do not, and what ships for them is an unspec'd placeholder pending design input:
      • Compact Slim (narrow one-third split or slim cover screen): reuses the compact bar, with tight horizontal width.
      • Compact Tall (compact width at tablet height, e.g. a narrow split on a tablet): reuses the compact bar, with extra vertical letterbox space. Also a layout-mode question: the classifier has seven modes and none for compact tall, so it may become an eighth.
      • Compact Short (50/50 phone split or flip cover screen): developer placeholder side rails.
      • Compact Short Slim (smallest quarter split or mini panes): developer placeholder side rails.
    • Drag-to-zoom direction on side-rail layouts: Today, after a long press on the shutter, zoom follows vertical movement above the shutter and sideways movement drives the recording lock switch, in every layout. When the shutter sits in a vertical side rail, it is an open question whether zoom should stay on the up/down axis or move to left/right.

Testing

  • CameraLayoutModeTest (10 JUnit4 tests): Initial 7-mode classification, rotation-pair invariants, isFullScreenCompactPhoneWindow edge cases (2.dp tolerance, multi-window, sw600dp, 360×360dp cover display), and bidirectional 14.dp hysteresis sweeps across all 6 boundaries.
  • AdaptiveCaptureLayoutTest (12 Robolectric Compose UI tests): Device-qualifier routing across all 7 modes, multi-window statusBars top-bar clearance, end-to-end CaptureScaffold on 1280×800dp TABLET_LANDSCAPE (real shutter/flip/carousel, vertical ZoomBar, clickable vertical mode-toggle placeholder, single-node Quick Settings card), and 10-step dynamic resize across all 7 modes verifying composeCount == 1 and disposeCount == 0.

@Kimblebee
Kimblebee changed the base branch from kim/adaptive/side-rails to kim/adaptive/layouts November 13, 2026 00:50

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the camera capture layout into an adaptive 7-mode orchestrator that dispatches across dedicated posture layouts (CompactPortraitLayout, CompactShortLayout, FoldableLayout, TabletPortraitLayout, and TabletLandscapeLayout) to support various device geometries, and adds comprehensive Robolectric tests. The review feedback suggests utilizing the existing CameraLayoutMode enum instead of raw string literals for active layout names, passing isErrorDialogShowing explicitly from CaptureScaffold for more robust state-based detection, and using placeRelative instead of place in custom layouts to ensure proper Right-to-Left (RTL) support.

I am having trouble creating individual review comments. Click here to see my feedback.

ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CompactPortraitLayout.kt (104-105)

medium

Using raw String literals (like "CompactPortraitLayout") for identifying active layouts is prone to typos and makes refactoring difficult. Since the codebase already has a well-defined CameraLayoutMode enum, and LocalCameraLayoutMode is already provided as a CompositionLocal, we can use it directly to determine if a compact layout is active (i.e., when the mode is COMPACT or COMPACT_SLIM). This eliminates the need for LocalActiveCaptureLayout entirely. If LocalActiveCaptureLayout must be kept for other reasons, it should at least be marked as internal to adhere to the visibility guidelines in the style guide.

References
  1. Proper Visibility Modifiers: Ensure all new functions, properties, and classes use the most restrictive visibility modifier possible (e.g., private, internal) while still allowing necessary access. Avoid public visibility unless explicitly required for external API exposure. (link)

ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CompactPortraitLayout.kt (335-336)

medium

Currently, isErrorDialogShowing is passed as a parameter to CompactPortraitLayout but is not supplied by CaptureScaffold when it invokes CaptureScaffoldLayout. As a result, isErrorDialogShowing will always default to false in production, making the layout entirely reliant on the onSizeChanged modifier of the errorDialog slot to detect if the error dialog is active. To make this state-based detection more robust and less reliant on layout-pass measurements, consider updating CaptureScaffold to pass isErrorDialogShowing = cameraErrorState.value is CameraErrorUiState.Showing to CaptureScaffoldLayout.

ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CompactShortLayout.kt (345-348)

medium

In custom layouts, it is idiomatic to use placeRelative instead of place to automatically support Right-to-Left (RTL) layouts correctly. Consider replacing place with placeRelative here to ensure consistency with other custom layouts in the codebase (such as AdaptivePortraitCaptureLayout).

                    placeable.placeRelative(
                        x = (resolvedWidth - placeable.width) / 2,
                        y = (resolvedHeight - placeable.height) / 2
                    )

@Kimblebee Kimblebee changed the title Kim/adaptive/layout orchestrator add 7-mode CameraLayoutMode orchestrator and wire adaptive CaptureScaffold slots Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant