Repository navigation
Conversation
…tangular overlays
…tangular overlays
…to kim/adaptive/content-gravity
…tureScaffold Co-authored-by: davidjiagoogle <[email protected]>
There was a problem hiding this comment.
Code Review
This pull request implements dynamic screen orientation and window rotation support, allowing the camera preview and UI to adapt to different window postures and multi-window modes. Key changes include adding resolveRequestedOrientation and applyWindowRotation logic, updating MainActivity and CameraXCameraSystem to handle configuration changes, and adding comprehensive unit tests. The review feedback highlights the need to guard isInMultiWindowMode with an SDK version check to prevent crashes on older Android versions and requests the addition of a @RunWith annotation to DeviceRotationTest to adhere to the repository's style guide.
I am having trouble creating individual review comments. Click here to see my feedback.
app/src/main/java/com/google/jetpackcamera/MainActivity.kt (230)
The property Activity.isInMultiWindowMode was introduced in Android Nougat (API 24). If the application is run on devices with API levels below 24 (such as API 21-23), accessing this property directly as a default parameter value will result in a NoSuchMethodError or NoClassDefFoundError at runtime.\n\nTo prevent potential crashes on older Android versions, please guard the access to isInMultiWindowMode with an SDK version check.
multiWindowMode: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode
core/model/src/test/java/com/google/jetpackcamera/model/DeviceRotationTest.kt (20-22)
According to the repository style guide (line 61), all test classes must be annotated with @RunWith(...) to explicitly declare which test runner should be used. Since DeviceRotationTest does not have any Android dependencies other than the inlined Surface constants, it should be annotated with @RunWith(JUnit4::class) to comply with the style guide and ensure explicit test runner declaration.
import org.junit.Test\nimport org.junit.runner.RunWith\nimport org.junit.runners.JUnit4\n\n@RunWith(JUnit4::class)\nclass DeviceRotationTest {References
- All test classes must be annotated with
@RunWith(...)to explicitly declare which test runner should be used (e.g.,@RunWith(AndroidJUnit4::class),@RunWith(RobolectricTestRunner::class), or@RunWith(JUnit4::class)for host tests with no Android dependencies). (link)
Summary
Completes the Adaptive Capture rotation stack by orienting compact phone controls, labels, canvas icons, and rectangular overlays upright with physical gravity while keeping adaptive side-rail layouts locked to
0°. Onkim/adaptive/viewfinder-rotation,CompactPortraitLayoutheld its structural chrome in portrait geometry and kept the CameraX viewfinder aligned with the window, but individual icons,ZoomBarlabels,ToggleSwitchglyphs,CameraErrorDialog,PillSnackbarHost/ElapsedTimeText, and the Quick Settings bottom sheet still rendered sideways in landscape (±90°) and snapped back to0°when tilted to180°. This slice adds draw-phase upright rotation for leaf glyphs, constraint-swapping layout rotation for rectangular blocks, sticky180°gravity retention,90°quarter-turn animation with immediate180°flip snapping, and posture-scoped0°gravity enforcement across:core:modeland:ui:components:capture.Key Changes
180°Gravity & Quarter-Turn Animation State (DeviceRotation.kt,RotationModifiers.kt,CaptureLayout.kt)fallbackOn180Degrees: Int = 0toDeviceRotation.toGravityRotationDegreesand introducerememberStickyGravityTargetDegreesso holding the device at180°retains the previous upright gravity (0°,90°, or270°) and resets whenwindowRotationchanges.rememberGravityRotationStateanimating90°quarter-turns by default (animateQuarterTurns = true,250mstween), while snapping immediately on180°landscape-to-landscape flips (90° <-> 270°), window-rotation changes, or whenanimateQuarterTurns = false.Modifier.rotateLayoutToGravity()(viaderivedStateOf+snapToQuadrantDegrees) with a0°no-layer fast path, and clampLocalGravityRotationStatetoDefaultGravityRotationState(0f) for all non-CompactPortraitLayoutmodes inCaptureScaffoldLayout.CompactPortraitLayout.kt,CameraErrorDialog.kt,QuickSettingsScreen.kt,CaptureScaffold.kt,SideRailCaptureLayouts.kt)PillSnackbarHost+ElapsedTimeTextintoLandscapeTopEdgeBanneratAbsoluteAlignment.CenterRight(90°/Rotated270) orAbsoluteAlignment.CenterLeft(270°/Rotated90) using.rotateLayoutToGravity(targetGravityDegrees).CompactPortraitLayout's Quick Settings sheet via.rotateLayoutToGravitywith a380.dpportrait-height cap (maxWidth = 380.dpin landscape), orthogonal nested-scroll isolation (consumeRemainingScrollConnection), and an open-sheet cross-fade (150msfade out ->250msanimateContentSize->150msfade in)..verticalScroll(rememberScrollState())and24.dplandscape top padding toQuickSettingsScaffoldContent, unifyCaptureScaffoldDefaults.QuickSettingsonto it across compact and floating-card layouts, and apply.rotateLayoutToGravity()toCameraErrorDialog'sSurface.ZoomBarLabels &ToggleSwitchCanvas Icons (CaptureScreenComponents.kt,CaptureButtonComponents.kt,ImageWell.kt,QuickSettingsComponents.kt,ZoomBarComponents.kt,ToggleSwitch.kt)Modifier.animateToUpright()toCameraControlIconButton,PauseResumeToggleButton,AmplitudeToggleButton,StabilizationIcon,VideoQualityIcon,FlipCameraButtoninnerIcon,LockSwitchCaptureButtonNucleuslockIcon,ImageWellthumbnailImage,ToggleQuickSettingsButton,HdrIndicator, andTopBarQuickSettingIcon.ZoomBarItemlabels upright viagraphicsLayerwith sinusoidal dynamic fit-scaling (lerp(44.dp, 34.dp, |sin(θ)|), min scale0.6f).ToggleSwitchleft/right painters around each icon's center insidedrawClippedSwitchIconswhile keepingthumbRecttwo-toneclipRectbounds in unrotated track coordinates.Technical Notes
1:1glyphs and stationary-container icons usegraphicsLayer(animateToUpright) so layout slot sizes never change (0sibling shift). Rectangular blocks (CameraErrorDialog,LandscapeTopEdgeBanner, landscape Quick Settings) useModifier.rotateLayoutToGravity()to swap(minWidth, maxWidth) <-> (minHeight, maxHeight)before measurement and center viaplaceWithLayer, keeping text wrapping unclipped and hit targets aligned with rotated visuals while avoiding extra layer allocation at0°.180°flip snap:rememberGravityRotationStateanimates90°quarter-turns over250msby default (animateQuarterTurns = true), while snapping immediately on180°half-turn flips (rawDelta == ±180f) or whenanimateQuarterTurns = false.NestedScrollDispatcherdoes not rotateOffset/Velocityvectors acrossgraphicsLayer.rotationZ = ±90f,consumeRemainingScrollConnectionabsorbs post-scroll/post-fling remainders so vertical list overscroll inside the rotated landscape sheet cannot drag or dismissBottomSheetScaffoldalong its orthogonal portrait-Y axis.800x400dphorizontal compact split-screen (windowRotation = Natural,shouldCounterRotate = true),.counterRotateToPortraitrotates-90°CCW while interior gravity resolves+90°CW (containerFrameRotation = Rotated90), bringing controls and dialogs back to0°upright on screen.Pre-Existing Bugs Fixed
180°spin on+90° -> 270°transitions (RotationModifiers.kt):rememberGravityRotationStatenow snaps180°half-turn flips immediately while animating90°quarter-turns (animateQuarterTurns = true).QuickSettingsScaffoldContent& duplicate scroll wrappers: Moving.verticalScroll(rememberScrollState())intoQuickSettingsScaffoldContent, unifyingCaptureScaffoldDefaults.QuickSettings, and removing the outerverticalScrollBoxinQuickSettingsOverlayCardPlaceholderfixes vertical scrolling across both compact sheets and short/side-rail floating cards without nested-scroll crashes.Not Included Yet
CaptureModeToggleButton/ToggleSwitch(#635),QuickSettingsOverlayCard(#648), andVerticalIndicatorPill(#649) remain tracked in their dedicated leaf PRs; side-rail layouts enforceLocalGravityRotationState = 0fso these components compose cleanly without conflict.#622) and zoom bar right-to-left polish (#644) are tracked in their own PRs.Testing
./gradlew :core:model:testDebugUnitTest --tests "*DeviceRotationTest*"— Default0°and statefulfallbackOn180Degrees(90,270) behavior at180°deltas../gradlew :ui:components:capture:testDebugUnitTest --tests "*RotationModifiersTest*" --tests "*ContentGravityTest*"— Sticky180°state transitions, defaultanimateQuarterTurns = true(90°tween /180°snap) andanimateQuarterTurns = falseimmediate snap,1:1slot-bounds invariance, single-rotation intopStartContent, unclippedCameraErrorDialog&LandscapeTopEdgeBanner(LTR + RTL),380.dplandscape Quick Settings sheet swap,0°side-rail enforcement across all 4 rail/short layouts,800x400dpsplit-screen counter-rotation + interior gravity cancellation,ZoomBar"1.5×"label rotation, and@GraphicsMode(NATIVE)pixel verification ofToggleSwitchcenter-rotated icons with track-space thumb clipping.