Skip to content

Implement phone landscape mode with window counter-rotation and upright icon animations - #619

Open
davidjiagoogle wants to merge 6 commits into
mainfrom
david/phoneLandscapeMode
Open

davidjiagoogle wants to merge 6 commits into
mainfrom
david/phoneLandscapeMode

Conversation

@davidjiagoogle

@davidjiagoogle davidjiagoogle commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Implements phone landscape mode (Rotated90 and Rotated270) for Jetpack Camera App using root counter-rotation and upright icon animations while preserving JCA's UDF state pipeline:

  1. Manifest & Window Configuration (AndroidManifest.xml, MainActivity.kt):

    • Enables sensor-based orientation (android:screenOrientation="fullUser"), adds orientation|screenSize|screenLayout|smallestScreenSize to android:configChanges to avoid CameraSession re-binding on rotation, and sets rotationAnimation = ROTATION_ANIMATION_JUMPCUT for zero-frame-drop OS rotation transitions.
    • Tracks Configuration.orientation in MainActivity (isSystemInLandscape) to feed into CaptureUiState.
  2. Sensor Hysteresis & Shortest-Path Rotation (DeviceRotation.kt, RotationModifiers.kt, CameraSession.kt):

    • Adds a ±20° dead-zone (HYSTERESIS_DEGREES = 20) around 45°/135°/225°/315° thresholds in DeviceRotation.fromSensorDegrees(orientation, previousRotation) to prevent boundary flicker.
    • Adds Modifier.counterRotateWithSystem(deviceRotation, isSystemInLandscape) to counter-rotate the root container back to the portrait coordinate frame when the OS rotates to landscape, keeping controls anchored by the physical charging port and top bezel.
    • Adds Modifier.animateToUpright(deviceRotation) and Modifier.rotateLayout90() with shortest-path angle unwrapping (targetShortestAngle) so 270° ↔ 0° transitions smoothly animate across 90° instead of spinning 270°.
  3. Capture & Quick Settings UI Adaptation (CaptureLayout.kt, CaptureScreenComponents.kt, QuickSettingsScreen.kt, etc.):

    • Counter-rotates the root PreviewLayout and rotates individual icons/buttons (FlipCameraButton, ImageWell, QuickSettingsDropDown, FlashModeIndicator, HdrIndicator, zoom preset text labels, elapsed timer, snackbars, CameraErrorDialog, ToggleSwitch icons, and topStartContent) upright in place.
    • Shifts topStartContent to Alignment.TopEnd in standard landscape (DeviceRotation.Rotated270) so it stays at the visual top-left of the screen.
    • Adapts QuickSettingsScreenOverlay into a 410dp landscape side sheet (AnimatedVisibility horizontal slide + rotateLayout90) in landscape orientations.

Testing

  • Added unit tests in DeviceRotationTest.kt and CaptureUiStateAdapterTest.kt.
  • Verified Portrait (Natural), Standard Landscape (Rotated270), Reverse Landscape (Rotated90), and Landscape Quick Settings side sheet on Cuttlefish emulator.

@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 introduces comprehensive device and window rotation handling for the camera capture screen, including UI element counter-rotation, landscape layout adjustments, and orientation locking during video recording. The review feedback highlights a potential layout flicker issue in PreviewScreen.kt due to redundant orientation resets, which can be resolved by separating lifecycle cleanup from active state updates. Additionally, several visibility modifiers in RotationModifiers.kt should be restricted to internal to comply with the repository style guide, and the DeviceRotationTest class requires a @RunWith annotation.

@davidjiagoogle davidjiagoogle changed the title Implement phone landscape mode with GCA counter-rotation and upright icon animations Implement phone landscape mode with counter-rotation and upright icon animations Oct 20, 2026
@davidjiagoogle
davidjiagoogle force-pushed the david/phoneLandscapeMode branch from 185d5ca to cd67e1d Compare October 20, 2026 00:09
@davidjiagoogle davidjiagoogle changed the title Implement phone landscape mode with counter-rotation and upright icon animations Implement phone landscape mode with window counter-rotation and upright icon animations Oct 20, 2026
@davidjiagoogle
davidjiagoogle force-pushed the david/phoneLandscapeMode branch from cd67e1d to 19ed975 Compare October 20, 2026 02:34
@google google deleted a comment from gemini-code-assist Bot Oct 20, 2026
@google google deleted a comment from gemini-code-assist Bot Oct 20, 2026
@google google deleted a comment from gemini-code-assist Bot Oct 20, 2026
@google google deleted a comment from gemini-code-assist Bot Oct 20, 2026
Build.VERSION.SDK_INT > 24 -> ImplementationMode.EXTERNAL
else -> ImplementationMode.EMBEDDED
}
val implementationMode = ImplementationMode.EMBEDDED

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Switching this to ImplementationMode.EMBEDDED forces TextureView instead of SurfaceView, which causes a few problems:

When you first launch the app in STANDARD mode (Preview + ImageCapture + VideoCapture), CameraX uses StreamSharing and pre-rotates the buffers in OpenGL (hasCameraTransform = false / TransformationMode.PRE_APPLIED), so Viewfinder skips getTextureViewCorrectionMatrix and counterRotateToPortrait rotates the preview as expected. Once you switch modes (like switching to video mode or photo-only mode), StreamSharing isn't used anymore (hasCameraTransform = true / TransformationMode.DEFERRED), and the viewfinder gets stretched and mis-rotated in landscape because Viewfinder applies getTextureViewCorrectionMatrix(displayRotationDegrees) for the landscape window on top of counterRotateToPortrait while sourceRotation stays at ROTATION_0.

Using EMBEDDED also drops the SurfaceView hardware overlay path, and it breaks 10-bit HDR preview because DetectWindowColorModeChanges right above this (L732) checks implMode == ImplementationMode.EXTERNAL before setting COLOR_MODE_HDR (and TextureView can't render 10-bit HDR).

In #162, Preview.targetRotation was pinned to Surface.ROTATION_0 (72fa358c4c) because updating Preview.targetRotation dynamically on window rotation is asynchronous and lags behind the Compose configuration change. Back then, #162 kept PreviewDisplay outside rotatedLayout (counterRotateToPortrait), stayed on ImplementationMode.EXTERNAL, and adjusted transformationInfo.sourceRotation synchronously during composition using LocalView.current.display.rotation (73dcf435ea, when JCA had its own wrapper around Viewfinder).

Now that we use androidx.camera.compose.CameraXViewfinder, we don't have a way to intercept TransformationInfo in JCA, and Viewfinder / StreamSharing in CameraX don't currently handle dynamic display rotation cleanly across both PRE_APPLIED (StreamSharing) and DEFERRED (direct camera) streams without either stretching the preview, lagging on window rotation, or breaking ImplementationMode.EXTERNAL (and 10-bit HDR). I am going to file bugs against CameraX to get this fixed upstream first, since we'll need CameraX support before we can properly support this flow.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Is there anything in this PR that should be done regarding this? Or just wait for the CameraX fix?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm looking into how easy this would be to fix in CameraX. I'll let you know.

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.

3 participants