diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ea2b4cb0..10ea2350 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,7 +34,7 @@ secrets { android { namespace = "com.google.android.samples.socialite" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "com.google.android.samples.socialite" @@ -125,10 +125,10 @@ dependencies { implementation(libs.concurrent.kts) implementation(libs.camera.core) + implementation(libs.camera.compose) implementation(libs.camera2) implementation(libs.camera.lifecycle) implementation(libs.camera.view) - implementation(libs.camera.viewfinder.compose) implementation(libs.media3.common) implementation(libs.media3.effect) diff --git a/app/src/main/java/com/google/android/samples/socialite/ui/camera/Camera.kt b/app/src/main/java/com/google/android/samples/socialite/ui/camera/Camera.kt index fd81f344..823ed8a7 100644 --- a/app/src/main/java/com/google/android/samples/socialite/ui/camera/Camera.kt +++ b/app/src/main/java/com/google/android/samples/socialite/ui/camera/Camera.kt @@ -109,6 +109,7 @@ fun Camera( } val viewFinderState by viewModel.viewFinderState.collectAsStateWithLifecycle() + val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle() var rotation by remember { mutableStateOf(Surface.ROTATION_0) } DisposableEffect(lifecycleOwner, context) { @@ -126,40 +127,31 @@ fun Camera( rotationProvider.addListener(Dispatchers.Main.asExecutor(), rotationListener) + viewModel.startPreview(lifecycleOwner, captureMode, cameraSelector, rotation) + onDispose { rotationProvider.removeListener(rotationListener) } } - val onPreviewSurfaceProviderReady: (Preview.SurfaceProvider) -> Unit = { - surfaceProvider = it - viewModel.startPreview(lifecycleOwner, it, captureMode, cameraSelector, rotation) - } - fun setCaptureMode(mode: CaptureMode) { captureMode = mode - surfaceProvider?.let { provider -> - viewModel.startPreview( - lifecycleOwner, - provider, - captureMode, - cameraSelector, - rotation, - ) - } + viewModel.startPreview( + lifecycleOwner, + captureMode, + cameraSelector, + rotation, + ) } fun setCameraSelector(selector: CameraSelector) { cameraSelector = selector - surfaceProvider?.let { provider -> - viewModel.startPreview( - lifecycleOwner, - provider, - captureMode, - cameraSelector, - rotation, - ) - } + viewModel.startPreview( + lifecycleOwner, + captureMode, + cameraSelector, + rotation, + ) } @SuppressLint("MissingPermission") @@ -249,7 +241,7 @@ fun Camera( ) { ViewFinder( viewFinderState.cameraState, - onPreviewSurfaceProviderReady, + surfaceRequest, viewModel::setZoomScale, ) } @@ -262,7 +254,7 @@ fun Camera( ) { ViewFinder( viewFinderState.cameraState, - onPreviewSurfaceProviderReady, + surfaceRequest, viewModel::setZoomScale, ) } @@ -311,7 +303,11 @@ fun Camera( } @Composable -fun CameraControls(captureMode: CaptureMode, onPhotoButtonClick: () -> Unit, onVideoButtonClick: () -> Unit) { +fun CameraControls( + captureMode: CaptureMode, + onPhotoButtonClick: () -> Unit, + onVideoButtonClick: () -> Unit, +) { val activeButtonColor = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary) val inactiveButtonColor = @@ -335,7 +331,12 @@ fun CameraControls(captureMode: CaptureMode, onPhotoButtonClick: () -> Unit, onV } @Composable -fun ShutterButton(captureMode: CaptureMode, onPhotoCapture: () -> Unit, onVideoRecordingStart: () -> Unit, onVideoRecordingFinish: () -> Unit) { +fun ShutterButton( + captureMode: CaptureMode, + onPhotoCapture: () -> Unit, + onVideoRecordingStart: () -> Unit, + onVideoRecordingFinish: () -> Unit, +) { Box(modifier = Modifier.padding(25.dp, 0.dp)) { if (captureMode == CaptureMode.PHOTO) { Button( @@ -370,15 +371,21 @@ fun ShutterButton(captureMode: CaptureMode, onPhotoCapture: () -> Unit, onVideoR } @Composable -fun CameraSwitcher(captureMode: CaptureMode, cameraSelector: CameraSelector, setCameraSelector: KFunction1) { +fun CameraSwitcher( + captureMode: CaptureMode, + cameraSelector: CameraSelector, + setCameraSelector: KFunction1, +) { if (captureMode != CaptureMode.VIDEO_RECORDING) { - IconButton(onClick = { - if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { - setCameraSelector(CameraSelector.DEFAULT_FRONT_CAMERA) - } else { - setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA) - } - }) { + IconButton( + onClick = { + if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { + setCameraSelector(CameraSelector.DEFAULT_FRONT_CAMERA) + } else { + setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA) + } + }, + ) { Icon( imageVector = Icons.Default.Autorenew, contentDescription = null, diff --git a/app/src/main/java/com/google/android/samples/socialite/ui/camera/CameraViewModel.kt b/app/src/main/java/com/google/android/samples/socialite/ui/camera/CameraViewModel.kt index adc43832..d695fbb2 100644 --- a/app/src/main/java/com/google/android/samples/socialite/ui/camera/CameraViewModel.kt +++ b/app/src/main/java/com/google/android/samples/socialite/ui/camera/CameraViewModel.kt @@ -34,6 +34,7 @@ import androidx.camera.core.FocusMeteringAction import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException import androidx.camera.core.Preview +import androidx.camera.core.SurfaceRequest import androidx.camera.core.UseCaseGroup import androidx.camera.core.resolutionselector.AspectRatioStrategy import androidx.camera.core.resolutionselector.ResolutionSelector @@ -60,6 +61,7 @@ import java.text.SimpleDateFormat import java.util.Locale import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch private const val TAG = "CameraViewModel" @@ -84,9 +86,17 @@ class CameraViewModel @Inject constructor( .setAspectRatioStrategy(aspectRatioStrategy) .build() + private val _surfaceRequest = MutableStateFlow(null) + val surfaceRequest: StateFlow = _surfaceRequest + private val previewUseCase = Preview.Builder() .setResolutionSelector(resolutionSelector) - .build() + .build().apply { + setSurfaceProvider { newSurfaceRequest -> + Log.d("JOLO", "New surface request") + _surfaceRequest.value = newSurfaceRequest + } + } private val imageCaptureUseCase = ImageCapture.Builder() .setResolutionSelector(resolutionSelector) @@ -141,7 +151,6 @@ class CameraViewModel @Inject constructor( fun startPreview( lifecycleOwner: LifecycleOwner, - surfaceProvider: Preview.SurfaceProvider, captureMode: CaptureMode, cameraSelector: CameraSelector, rotation: Int, @@ -156,9 +165,7 @@ class CameraViewModel @Inject constructor( } var extensionsCameraSelector: CameraSelector? = null val useCaseGroupBuilder = UseCaseGroup.Builder() - - previewUseCase.setSurfaceProvider(surfaceProvider) - useCaseGroupBuilder.addUseCase(previewUseCase) + .addUseCase(previewUseCase) if (captureMode == CaptureMode.PHOTO) { try { diff --git a/app/src/main/java/com/google/android/samples/socialite/ui/camera/CameraXViewfinder.kt b/app/src/main/java/com/google/android/samples/socialite/ui/camera/CameraXViewfinder.kt deleted file mode 100644 index 643bde5b..00000000 --- a/app/src/main/java/com/google/android/samples/socialite/ui/camera/CameraXViewfinder.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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.android.samples.socialite.ui.camera - -import android.annotation.SuppressLint -import androidx.camera.core.Preview -import androidx.camera.core.SurfaceRequest -import androidx.camera.core.SurfaceRequest.TransformationInfo as CXTransformationInfo -import androidx.camera.viewfinder.compose.Viewfinder -import androidx.camera.viewfinder.surface.ImplementationMode -import androidx.camera.viewfinder.surface.TransformationInfo -import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.compose.ui.Modifier -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Runnable -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -/** - * A composable viewfinder that adapts CameraX's [Preview.SurfaceProvider] to [Viewfinder] - * - * This adapter code will eventually be upstreamed to CameraX, but for now can be copied - * in its entirety to connect CameraX to [Viewfinder]. - * - * @param[modifier] the modifier to be applied to the layout - * @param[implementationMode] the implementation mode, either [ImplementationMode.PERFORMANCE] or - * [ImplementationMode.COMPATIBLE]. Currently, only [ImplementationMode.PERFORMANCE] will produce - * the correct orientation. - * @param[onSurfaceProviderReady] a callback to retrieve a [Preview.SurfaceProvider] that can be - * set on [Preview.setSurfaceProvider]. This callback will be called with a new - * [Preview.SurfaceProvider] if a new [ImplementationMode] is provided. - */ -@SuppressLint("RestrictedApi") -@Composable -fun CameraXViewfinder( - modifier: Modifier = Modifier, - implementationMode: ImplementationMode = ImplementationMode.PERFORMANCE, - onSurfaceProviderReady: (Preview.SurfaceProvider) -> Unit = {}, -) { - val viewfinderArgs by produceState(initialValue = null, implementationMode) { - val requests = MutableStateFlow(null) - onSurfaceProviderReady( - Preview.SurfaceProvider { request -> - requests.update { oldRequest -> - oldRequest?.willNotProvideSurface() - request - } - }, - ) - - requests.filterNotNull().collectLatest { request -> - val viewfinderSurfaceRequest = ViewfinderSurfaceRequest.Builder(request.resolution) - .build() - - request.addRequestCancellationListener(Runnable::run) { - viewfinderSurfaceRequest.markSurfaceSafeToRelease() - } - - // Launch undispatched so we always reach the try/finally in this coroutine - launch(start = CoroutineStart.UNDISPATCHED) { - try { - val surface = viewfinderSurfaceRequest.getSurface() - request.provideSurface(surface, Runnable::run) { - viewfinderSurfaceRequest.markSurfaceSafeToRelease() - } - } finally { - // If we haven't provided the surface, such as if we're cancelled - // while suspending on getSurface(), this call will succeed. Otherwise - // it will be a no-op. - request.willNotProvideSurface() - } - } - - val transformationInfos = MutableStateFlow(null) - request.setTransformationInfoListener(Runnable::run) { - transformationInfos.value = it - } - - transformationInfos.filterNotNull().collectLatest { - value = ViewfinderArgs( - viewfinderSurfaceRequest, - implementationMode, - TransformationInfo( - it.rotationDegrees, - it.cropRect.left, - it.cropRect.right, - it.cropRect.top, - it.cropRect.bottom, - it.isMirroring, - ), - ) - } - } - } - - viewfinderArgs?.let { args -> - Viewfinder( - surfaceRequest = args.viewfinderSurfaceRequest, - implementationMode = args.implementationMode, - transformationInfo = args.transformationInfo, - modifier = modifier.fillMaxSize(), - ) - } -} - -private data class ViewfinderArgs( - val viewfinderSurfaceRequest: ViewfinderSurfaceRequest, - val implementationMode: ImplementationMode, - val transformationInfo: TransformationInfo, -) diff --git a/app/src/main/java/com/google/android/samples/socialite/ui/camera/ViewFinder.kt b/app/src/main/java/com/google/android/samples/socialite/ui/camera/ViewFinder.kt index e9e4a3a9..3722c2dc 100644 --- a/app/src/main/java/com/google/android/samples/socialite/ui/camera/ViewFinder.kt +++ b/app/src/main/java/com/google/android/samples/socialite/ui/camera/ViewFinder.kt @@ -16,7 +16,8 @@ package com.google.android.samples.socialite.ui.camera -import androidx.camera.core.Preview +import androidx.camera.compose.CameraXViewfinder +import androidx.camera.core.SurfaceRequest import androidx.camera.viewfinder.surface.ImplementationMode import androidx.compose.foundation.background import androidx.compose.foundation.gestures.rememberTransformableState @@ -31,7 +32,7 @@ import androidx.compose.ui.graphics.Color @Composable fun ViewFinder( cameraState: CameraState, - onSurfaceProviderReady: (Preview.SurfaceProvider) -> Unit = {}, + surfaceRequest: SurfaceRequest?, onZoomChange: (Float) -> Unit, ) { val transformableState = rememberTransformableState( @@ -49,11 +50,13 @@ fun ViewFinder( modifier = Modifier .transformable(state = transformableState), ) { - CameraXViewfinder( - modifier = Modifier.fillMaxSize(), - implementationMode = ImplementationMode.PERFORMANCE, - onSurfaceProviderReady = onSurfaceProviderReady, - ) + surfaceRequest?.let { + CameraXViewfinder( + modifier = Modifier.fillMaxSize(), + implementationMode = ImplementationMode.EXTERNAL, + surfaceRequest = surfaceRequest, + ) + } } } } diff --git a/baselineprofile/build.gradle.kts b/baselineprofile/build.gradle.kts index b0150856..bdbe8caa 100644 --- a/baselineprofile/build.gradle.kts +++ b/baselineprofile/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "com.google.android.samples.socialite.baselineprofile" - compileSdk = 34 + compileSdk = 35 compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3de87f20..e06a0420 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,12 +18,10 @@ activity = "1.9.3" agp = "8.2.2" benchmarkMacroJunit4 = "1.2.3" baselineprofile = "1.2.3" -camera = "1.4.0-SNAPSHOT" -cameraViewfinderCompose = "1.0.0-SNAPSHOT" +camera = "1.5.0-alpha03" coil = "2.4.0" compose_bom = "2024.04.00" composeCompiler = "1.5.4" # Used in app/build.gradle.kts -compose-foundation = "1.6.0-beta03" concurrent = "1.1.0" core = "1.12.0" core-splashscreen = "1.0.1" @@ -60,11 +58,11 @@ activity = { group = "androidx.activity", name = "activity", version.ref = "acti activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" } benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camera" } +camera-compose = { group = "androidx.camera", name = "camera-compose", version.ref = "camera"} camera-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "camera" } camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camera" } camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camera" } camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camera" } -camera-viewfinder-compose = { module = "androidx.camera:camera-viewfinder-compose", version.ref = "cameraViewfinderCompose" } coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }