Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smooth rotation on orientation change #162

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8a8e5c5
Update versions and version naming
JolandaVerhoef Mar 29, 2024
cdf76e0
Fixes #55. Latest ktlint and spotless do not complain about composabl…
JolandaVerhoef Mar 29, 2024
f366230
Merge branch 'main' into jv/cleanup
JolandaVerhoef Mar 29, 2024
bb84056
Get rid of lint warnings
JolandaVerhoef Mar 29, 2024
46a3b36
Fix Compose issues (mostly modifier order)
JolandaVerhoef Mar 29, 2024
297999f
Allow for a smooth rotation while running the app. Replaced the quick…
JolandaVerhoef Mar 29, 2024
ce9a178
Merge remote-tracking branch 'origin/main' into jv/rotation
temcguir Apr 19, 2024
24352dc
Merge remote-tracking branch 'origin/main' into jv/rotation
temcguir Apr 21, 2024
009a8b8
Merge remote-tracking branch 'origin/main' into jv/rotation
temcguir May 7, 2024
72fa358
Respond to display rotation so images and videos are captured in corr…
temcguir May 9, 2024
b0ae518
Reduce number of recompositions
temcguir May 8, 2024
73dcf43
Adjust source rotation relative to screen orientation inline
temcguir May 8, 2024
5867abb
apply spotless
temcguir May 9, 2024
778f611
Switch to using OrientationEventListener to set orientation of captur…
temcguir May 23, 2024
74256ab
Merge remote-tracking branch 'origin/main' into jv/rotation
temcguir May 23, 2024
10891dd
Rename DisplayRotation to DeviceRotation
temcguir May 23, 2024
7018558
Merge remote-tracking branch 'origin/main' into jv/rotation
temcguir May 24, 2024
7336f34
Merge remote-tracking branch 'origin/main' into jv/rotation
temcguir May 29, 2024
98baff4
Ensure orientation is initialized correctly
temcguir May 29, 2024
9832e2b
Merge remote-tracking branch 'origin/main' into jv/rotation
temcguir May 30, 2024
a93d753
Merge remote-tracking branch 'origin/main' into jv/rotation
temcguir Jun 5, 2024
f767fa5
Merge remote-tracking branch 'origin' into jv/rotation
temcguir Jun 11, 2024
71ce1ae
Rename setDisplayRotation -> setDeviceRotation in CameraUseCase
temcguir Jun 11, 2024
cbf4a78
Log device rotation changes in Camera coroutine
temcguir Jun 11, 2024
c63f30a
Restore previous rotation animation/system bars behavior in SmoothImm…
temcguir Jun 12, 2024
cf5094c
Make Modifier.rotateLayout apply immediately
temcguir Jun 12, 2024
b6165de
Make Modifier.rotatedLayout keep the layout of natural orientation
temcguir Jun 12, 2024
689b6ff
apply spotless
temcguir Jun 12, 2024
b0116ad
Merge remote-tracking branch 'origin' into jv/rotation
temcguir Jun 12, 2024
c777a3d
Pad bottom camera controls for insets
temcguir Jun 14, 2024
9d8aac5
Animate icons to rotate upright
temcguir Jun 15, 2024
54670bf
Animate quicksettings layout transitions
temcguir Jun 17, 2024
476d0b9
Make quicksettings centered after rotation
temcguir Jun 19, 2024
2e7eca9
Merge remote-tracking branch 'origin' into jv/rotation
temcguir Jun 19, 2024
b232e8d
Rotate audio amplitude icon when device is rotated
temcguir Jun 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
<profileable android:shell="true"/>
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode|smallestScreenSize"
android:exported="true"
android:screenOrientation="nosensor"
android:theme="@style/Theme.JetpackCamera">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
30 changes: 18 additions & 12 deletions app/src/main/java/com/google/jetpackcamera/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import android.provider.Settings
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
Expand All @@ -42,6 +43,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
Expand Down Expand Up @@ -78,6 +80,7 @@ class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.M)
@OptIn(ExperimentalComposeUiApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
var uiState: MainActivityUiState by mutableStateOf(Loading)

Expand Down Expand Up @@ -106,6 +109,7 @@ class MainActivity : ComponentActivity() {
}

is Success -> {
val previewMode = remember { getPreviewMode() }
// TODO(kimblebee@): add app setting to enable/disable dynamic color
JetpackCameraTheme(
darkTheme = isInDarkMode(uiState = uiState),
Expand All @@ -120,19 +124,9 @@ class MainActivity : ComponentActivity() {
color = MaterialTheme.colorScheme.background
) {
JcaApp(
previewMode = getPreviewMode(),
previewMode = previewMode,
openAppSettings = ::openAppSettings,
onRequestWindowColorMode = { colorMode ->
// Window color mode APIs require API level 26+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.d(
TAG,
"Setting window color mode to:" +
" ${colorMode.toColorModeString()}"
)
window?.colorMode = colorMode
}
}
onRequestWindowColorMode = ::setWindowColorMode
)
}
}
Expand Down Expand Up @@ -175,6 +169,18 @@ class MainActivity : ComponentActivity() {
}
}
}

private fun setWindowColorMode(colorMode: Int) {
// Window color mode APIs require API level 26+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.d(
TAG,
"Setting window color mode to:" +
" ${colorMode.toColorModeString()}"
)
window?.colorMode = colorMode
}
}
}

/**
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.google.jetpackcamera.ui
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
Expand Down Expand Up @@ -96,7 +97,11 @@ private fun JetpackCameraNavHost(
}
}
PreviewScreen(
onNavigateToSettings = { navController.navigate(SETTINGS_ROUTE) },
onNavigateToSettings = remember(navController) {
{
navController.navigate(SETTINGS_ROUTE)
}
},
onRequestWindowColorMode = onRequestWindowColorMode,
previewMode = previewMode
)
Expand All @@ -107,7 +112,7 @@ private fun JetpackCameraNavHost(
versionName = BuildConfig.VERSION_NAME,
buildType = BuildConfig.BUILD_TYPE
),
onNavigateBack = { navController.popBackStack() }
onNavigateBack = navController::popBackStack
)
}
}
Expand Down
1 change: 1 addition & 0 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ android {
// self instrumentation required for the tests to be able to compile, start, or kill the app
// ensures test and app processes are separate
// see https://source.android.com/docs/core/tests/development/instr-self-e2e
@Suppress("UnstableApiUsage")
experimentalProperties["android.experimental.self-instrumenting"] = true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ data class CameraAppSettings(
val zoomScale: Float = 1f,
val targetFrameRate: Int = TARGET_FPS_AUTO,
val imageFormat: ImageOutputFormat = ImageOutputFormat.JPEG,
val audioMuted: Boolean = false
val audioMuted: Boolean = false,
val deviceRotation: DeviceRotation = DeviceRotation.Natural
)

fun SystemConstraints.forCurrentLens(cameraAppSettings: CameraAppSettings): CameraConstraints? {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.jetpackcamera.settings.model

import android.view.Surface

enum class DeviceRotation {
Natural,
Rotated90,
Rotated180,
Rotated270;

/**
* Returns the rotation of the UI, expressed as a [Surface] rotation constant, needed to
* compensate for device rotation.
*
* These values do not match up with the device rotation angle. When the device is rotated,
* the UI must rotate in the opposite direction to compensate, so the angles 90 and 270 will
* be swapped in UI rotation compared to device rotation.
*/
fun toUiSurfaceRotation(): Int {
return when (this) {
Natural -> Surface.ROTATION_0
Rotated90 -> Surface.ROTATION_270
Rotated180 -> Surface.ROTATION_180
Rotated270 -> Surface.ROTATION_90
}
}
fun toClockwiseRotationDegrees(): Int {
return when (this) {
Natural -> 0
Rotated90 -> 90
Rotated180 -> 180
Rotated270 -> 270
}
}

companion object {
fun snapFrom(degrees: Int): DeviceRotation {
check(degrees in 0..359) {
"Degrees must be in the range [0, 360)"
}

return when (val snappedDegrees = ((degrees + 45) / 90 * 90) % 360) {
0 -> Natural
90 -> Rotated90
180 -> Rotated180
270 -> Rotated270
else -> throw IllegalStateException(
"Unexpected snapped degrees: $snappedDegrees" +
". Should be one of 0, 90, 180 or 270."
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.camera.core.SurfaceRequest
import com.google.jetpackcamera.settings.model.AspectRatio
import com.google.jetpackcamera.settings.model.CameraAppSettings
import com.google.jetpackcamera.settings.model.CaptureMode
import com.google.jetpackcamera.settings.model.DeviceRotation
import com.google.jetpackcamera.settings.model.DynamicRange
import com.google.jetpackcamera.settings.model.FlashMode
import com.google.jetpackcamera.settings.model.ImageOutputFormat
Expand Down Expand Up @@ -93,6 +94,8 @@ interface CameraUseCase {

suspend fun setDynamicRange(dynamicRange: DynamicRange)

fun setDeviceRotation(deviceRotation: DeviceRotation)

suspend fun setLowLightBoost(lowLightBoost: LowLightBoost)

suspend fun setImageFormat(imageFormat: ImageOutputFormat)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import com.google.jetpackcamera.settings.model.AspectRatio
import com.google.jetpackcamera.settings.model.CameraAppSettings
import com.google.jetpackcamera.settings.model.CameraConstraints
import com.google.jetpackcamera.settings.model.CaptureMode
import com.google.jetpackcamera.settings.model.DeviceRotation
import com.google.jetpackcamera.settings.model.DynamicRange
import com.google.jetpackcamera.settings.model.FlashMode
import com.google.jetpackcamera.settings.model.ImageOutputFormat
Expand Down Expand Up @@ -254,9 +255,10 @@ constructor(
* The use cases typically will not need to be re-bound.
*/
private data class TransientSessionSettings(
val audioMuted: Boolean,
val flashMode: FlashMode,
val zoomScale: Float,
val audioMuted: Boolean
val deviceRotation: DeviceRotation,
val zoomScale: Float
)

override suspend fun runCamera() = coroutineScope {
Expand All @@ -267,8 +269,9 @@ constructor(
.filterNotNull()
.map { currentCameraSettings ->
transientSettings.value = TransientSessionSettings(
flashMode = currentCameraSettings.flashMode,
audioMuted = currentCameraSettings.audioMuted,
flashMode = currentCameraSettings.flashMode,
deviceRotation = currentCameraSettings.deviceRotation,
zoomScale = currentCameraSettings.zoomScale
)

Expand Down Expand Up @@ -351,6 +354,36 @@ constructor(
)
}

if (prevTransientSettings.deviceRotation
!= newTransientSettings.deviceRotation
) {
Log.d(
TAG,
"Updating device rotation from " +
"${prevTransientSettings.deviceRotation} -> " +
"${newTransientSettings.deviceRotation}"
)
val targetRotation =
newTransientSettings.deviceRotation.toUiSurfaceRotation()
useCaseGroup.useCases.forEach {
when (it) {
is Preview -> {
// Preview rotation should always be natural orientation
// in order to support seamless handling of orientation
// configuration changes in UI
}

is ImageCapture -> {
it.targetRotation = targetRotation
}

is VideoCapture<*> -> {
it.targetRotation = targetRotation
}
}
}
}

prevTransientSettings = newTransientSettings
}
}
Expand Down Expand Up @@ -704,10 +737,14 @@ constructor(
)

return UseCaseGroup.Builder().apply {
Log.d(
TAG,
"Setting initial device rotation to ${initialTransientSettings.deviceRotation}"
)
setViewPort(
ViewPort.Builder(
sessionSettings.aspectRatio.ratio,
previewUseCase.targetRotation
initialTransientSettings.deviceRotation.toUiSurfaceRotation()
).build()
)
addUseCase(previewUseCase)
Expand All @@ -734,6 +771,12 @@ constructor(
}
}

override fun setDeviceRotation(deviceRotation: DeviceRotation) {
currentSettings.update { old ->
old?.copy(deviceRotation = deviceRotation)
}
}

override suspend fun setImageFormat(imageFormat: ImageOutputFormat) {
currentSettings.update { old ->
old?.copy(imageFormat = imageFormat)
Expand Down Expand Up @@ -824,7 +867,9 @@ constructor(
sessionSettings: PerpetualSessionSettings,
supportedStabilizationModes: Set<SupportedStabilizationMode>
): Preview {
val previewUseCaseBuilder = Preview.Builder()
val previewUseCaseBuilder = Preview.Builder().apply {
setTargetRotation(DeviceRotation.Natural.toUiSurfaceRotation())
}
// set preview stabilization
if (shouldPreviewBeStabilized(sessionSettings, supportedStabilizationModes)) {
previewUseCaseBuilder.setPreviewStabilizationEnabled(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.google.jetpackcamera.domain.camera.CameraUseCase
import com.google.jetpackcamera.settings.model.AspectRatio
import com.google.jetpackcamera.settings.model.CameraAppSettings
import com.google.jetpackcamera.settings.model.CaptureMode
import com.google.jetpackcamera.settings.model.DeviceRotation
import com.google.jetpackcamera.settings.model.DynamicRange
import com.google.jetpackcamera.settings.model.FlashMode
import com.google.jetpackcamera.settings.model.ImageOutputFormat
Expand Down Expand Up @@ -193,6 +194,12 @@ class FakeCameraUseCase(
}
}

override fun setDeviceRotation(deviceRotation: DeviceRotation) {
currentSettings.update { old ->
old.copy(deviceRotation = deviceRotation)
}
}

override suspend fun setImageFormat(imageFormat: ImageOutputFormat) {
currentSettings.update { old ->
old.copy(imageFormat = imageFormat)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
package com.google.jetpackcamera.feature.preview

import android.net.Uri
import androidx.compose.runtime.Stable

/**
* This interface is determined before the Preview UI is launched and passed into PreviewScreen. The
* UX differs depends on which mode the Preview is launched under.
*/
@Stable
sealed interface PreviewMode {
/**
* The default mode for the app.
Expand Down
Loading
Loading