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

Use camera-compose and restore double-tap to flip camera #263

Merged
merged 4 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 feature/preview/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ dependencies {

// CameraX
implementation(libs.camera.core)
implementation(libs.camera.viewfinder.compose)
implementation(libs.camera.compose)

// Hilt
implementation(libs.dagger.hilt.android)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -203,15 +204,11 @@ private fun ContentScreen(
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) {
val lensFacing = remember(previewUiState) {
val lensFacing by rememberUpdatedState(
previewUiState.currentCameraSettings.cameraLensFacing
}
)

val onFlipCamera = remember(lensFacing) {
{
onSetLensFacing(lensFacing.flip())
}
}
val onFlipCamera = { onSetLensFacing(lensFacing.flip()) }

val isMuted = remember(previewUiState) {
previewUiState.currentCameraSettings.audioMuted
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
*/
package com.google.jetpackcamera.feature.preview.ui

import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.camera.compose.CameraXViewfinder
import androidx.camera.core.DynamicRange as CXDynamicRange
import androidx.camera.core.SurfaceRequest
import androidx.camera.viewfinder.compose.MutableCoordinateTransformer
import androidx.camera.viewfinder.surface.ImplementationMode
import androidx.compose.animation.core.EaseOutExpo
import androidx.compose.animation.core.LinearEasing
Expand Down Expand Up @@ -51,7 +55,6 @@ import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.FlipCameraAndroid
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.MicOff
import androidx.compose.material.icons.filled.Nightlight
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.VideoStable
import androidx.compose.material.icons.filled.Videocam
Expand All @@ -75,6 +78,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
Expand All @@ -98,6 +102,10 @@ import com.google.jetpackcamera.settings.model.AspectRatio
import com.google.jetpackcamera.settings.model.LowLightBoost
import com.google.jetpackcamera.settings.model.Stabilization
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch

private const val TAG = "PreviewScreen"
Expand Down Expand Up @@ -264,24 +272,12 @@ fun PreviewDisplay(
}
)

val currentOnFlipCamera by rememberUpdatedState(onFlipCamera)

surfaceRequest?.let {
BoxWithConstraints(
Modifier
.testTag(PREVIEW_DISPLAY)
.fillMaxSize()
.background(Color.Black)
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = { offset ->
// double tap to flip camera
Log.d(TAG, "onDoubleTap $offset")
currentOnFlipCamera()
}
)
},

.background(Color.Black),
contentAlignment = Alignment.Center
) {
val maxAspectRatio: Float = maxWidth / maxHeight
Expand Down Expand Up @@ -316,21 +312,94 @@ fun PreviewDisplay(
.alpha(imageAlpha)
.clip(RoundedCornerShape(16.dp))
) {
val implementationMode = when {
Build.VERSION.SDK_INT > 24 -> ImplementationMode.EXTERNAL
else -> ImplementationMode.EMBEDDED
}

DetectWindowColorModeChanges(
surfaceRequest = surfaceRequest,
implementationMode = implementationMode,
onRequestWindowColorMode = onRequestWindowColorMode
)

val coordinateTransformer = remember { MutableCoordinateTransformer() }
CameraXViewfinder(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = { offset ->
// double tap to flip camera
Log.d(TAG, "onDoubleTap $offset")
onFlipCamera()
},
onTap = {
with(coordinateTransformer) {
val surfaceCoords = it.transform()
Log.d(
"TAG",
"onTapToFocus: " +
"input{$it} -> surface{$surfaceCoords}"
)
onTapToFocus(surfaceCoords.x, surfaceCoords.y)
}
}
)
},
surfaceRequest = it,
implementationMode = when {
Build.VERSION.SDK_INT > 24 -> ImplementationMode.EXTERNAL
else -> ImplementationMode.EMBEDDED
},
onRequestWindowColorMode = onRequestWindowColorMode,
onTap = { x, y -> onTapToFocus(x, y) }
implementationMode = implementationMode,
coordinateTransformer = coordinateTransformer
)
}
}
}
}

@Composable
fun DetectWindowColorModeChanges(
surfaceRequest: SurfaceRequest,
implementationMode: ImplementationMode,
onRequestWindowColorMode: (Int) -> Unit
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val currentSurfaceRequest: SurfaceRequest by rememberUpdatedState(surfaceRequest)
val currentImplementationMode: ImplementationMode by rememberUpdatedState(
implementationMode
)
val currentOnRequestWindowColorMode: (Int) -> Unit by rememberUpdatedState(
onRequestWindowColorMode
)

LaunchedEffect(Unit) {
val colorModeSnapshotFlow =
snapshotFlow { Pair(currentSurfaceRequest.dynamicRange, currentImplementationMode) }
.map { (dynamicRange, implMode) ->
val isSourceHdr = dynamicRange.encoding != CXDynamicRange.ENCODING_SDR
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any possible isSourceHdr would be not HDR Encoding, even it is not Encoding SDR, such as Encoding Dolby Vision, Encoding HLG, Encoding Unspecified?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Dolby Vision, HLG10, HDR10 and HDR10+ are all considered HDR formats. Technically, the DynamicRange here from CameraX should never be UNSPECIFIED, since it is coming from SurfaceRequest. I'll make a note to update the CameraX documentation to better communicate that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The CameraX documentation improvements will be tracked with https://issuetracker.google.com/366536534

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for everything.

val destSupportsHdr = implMode == ImplementationMode.EXTERNAL
if (isSourceHdr && destSupportsHdr) {
ActivityInfo.COLOR_MODE_HDR
} else {
ActivityInfo.COLOR_MODE_DEFAULT
}
}.distinctUntilChanged()

val callbackSnapshotFlow = snapshotFlow { currentOnRequestWindowColorMode }

// Combine both flows so that we call the callback every time it changes or the
// window color mode changes.
// We'll also reset to default when this LaunchedEffect is disposed
combine(colorModeSnapshotFlow, callbackSnapshotFlow) { colorMode, callback ->
Pair(colorMode, callback)
}.onCompletion {
currentOnRequestWindowColorMode(ActivityInfo.COLOR_MODE_DEFAULT)
}.collect { (colorMode, callback) ->
callback(colorMode)
}
}
}
}

@Composable
fun StabilizationIcon(
videoStabilization: Stabilization,
Expand Down
Loading
Loading