From de4019cea75caac7bdad73f792f0301623635d29 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 9 Feb 2024 12:24:23 -0800 Subject: [PATCH] Migrate to camera-viewfinder-compose in CameraX (#106) * Migrate to camera-viewfinder-compose in CameraX * Removes mirror of camera-viewfinder-compose and replaces it with a dependency on the CameraX artifact * Adds @Composable adapter, CameraXViewfinder, that connects CameraX's Preview use case to Viewfinder * Removes tao-to-focus code as we no longer can retrieve the View from the composable. This code will need to be replaced by touch event callbacks that will be added to the Viewfinder composable in a future update. * Apply spotless for copyright headers --- camera-viewfinder-compose/.gitignore | 1 - camera-viewfinder-compose/consumer-rules.pro | 0 camera-viewfinder-compose/proguard-rules.pro | 21 -- .../src/main/AndroidManifest.xml | 19 -- .../surface/SurfaceTransformationUtil.kt | 211 ------------------ domain/camera/build.gradle.kts | 3 +- feature/preview/build.gradle.kts | 5 +- .../feature/preview/ui/CameraXViewfinder.kt | 128 +++++++++++ .../preview/ui/PreviewScreenComponents.kt | 31 +-- gradle/libs.versions.toml | 5 +- settings.gradle.kts | 3 +- 11 files changed, 138 insertions(+), 289 deletions(-) delete mode 100644 camera-viewfinder-compose/.gitignore delete mode 100644 camera-viewfinder-compose/consumer-rules.pro delete mode 100644 camera-viewfinder-compose/proguard-rules.pro delete mode 100644 camera-viewfinder-compose/src/main/AndroidManifest.xml delete mode 100644 camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/SurfaceTransformationUtil.kt create mode 100644 feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraXViewfinder.kt diff --git a/camera-viewfinder-compose/.gitignore b/camera-viewfinder-compose/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/camera-viewfinder-compose/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/camera-viewfinder-compose/consumer-rules.pro b/camera-viewfinder-compose/consumer-rules.pro deleted file mode 100644 index e69de29bb..000000000 diff --git a/camera-viewfinder-compose/proguard-rules.pro b/camera-viewfinder-compose/proguard-rules.pro deleted file mode 100644 index ff59496d8..000000000 --- a/camera-viewfinder-compose/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/camera-viewfinder-compose/src/main/AndroidManifest.xml b/camera-viewfinder-compose/src/main/AndroidManifest.xml deleted file mode 100644 index 5c675bbee..000000000 --- a/camera-viewfinder-compose/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/SurfaceTransformationUtil.kt b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/SurfaceTransformationUtil.kt deleted file mode 100644 index d8846ce79..000000000 --- a/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/SurfaceTransformationUtil.kt +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (C) 2023 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.viewfinder.surface - -import android.annotation.SuppressLint -import android.graphics.Matrix -import android.graphics.RectF -import android.util.Size -import androidx.camera.core.SurfaceRequest -import androidx.camera.core.impl.utils.CameraOrientationUtil -import androidx.camera.core.impl.utils.TransformUtils - -/** - * A util class with methods that transform the input viewFinder surface so that its preview fits - * the given aspect ratio of its parent view. - * - * The goal is to transform it in a way so that the entire area of - * [SurfaceRequest.TransformationInfo.getCropRect] is 1) visible to end users, and 2) - * displayed as large as possible. - * - * The inputs for the calculation are 1) the dimension of the Surface, 2) the crop rect, 3) the - * dimension of the Viewfinder and 4) rotation degrees - */ -object SurfaceTransformationUtil { - @SuppressLint("RestrictedApi", "WrongConstant") - private fun getRemainingRotationDegrees( - transformationInfo: SurfaceRequest.TransformationInfo - ): Int { - return if (!transformationInfo.hasCameraTransform()) { - // If the Surface is not connected to the camera, then the SurfaceView/TextureView will - // not apply any transformation. In that case, we need to apply the rotation - // calculated by CameraX. - transformationInfo.rotationDegrees - } else if (transformationInfo.targetRotation == -1) { - 0 - } else { - // If the Surface is connected to the camera, then the SurfaceView/TextureView - // will be the one to apply the camera orientation. In that case, only the Surface - // rotation needs to be applied. - -CameraOrientationUtil.surfaceRotationToDegrees(transformationInfo.targetRotation) - } - } - - @SuppressLint("RestrictedApi") - fun getTextureViewCorrectionMatrix( - transformationInfo: SurfaceRequest.TransformationInfo, - resolution: Size - ): Matrix { - val surfaceRect = - RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat()) - val rotationDegrees: Int = getRemainingRotationDegrees(transformationInfo) - return TransformUtils.getRectToRect(surfaceRect, surfaceRect, rotationDegrees) - } - - @SuppressLint("RestrictedApi") - private fun getRotatedViewportSize( - transformationInfo: SurfaceRequest.TransformationInfo - ): Size { - return if (TransformUtils.is90or270(transformationInfo.rotationDegrees)) { - Size(transformationInfo.cropRect.height(), transformationInfo.cropRect.width()) - } else { - Size(transformationInfo.cropRect.width(), transformationInfo.cropRect.height()) - } - } - - @SuppressLint("RestrictedApi") - fun isViewportAspectRatioMatchViewFinder( - transformationInfo: SurfaceRequest.TransformationInfo, - viewFinderSize: Size - ): Boolean { - // Using viewport rect to check if the viewport is based on the view finder. - val rotatedViewportSize: Size = getRotatedViewportSize(transformationInfo) - return TransformUtils.isAspectRatioMatchingWithRoundingError( - viewFinderSize, - true, - rotatedViewportSize, - false - ) - } - - private fun setMatrixRectToRect(matrix: Matrix, source: RectF, destination: RectF) { - val matrixScaleType = Matrix.ScaleToFit.CENTER - // android.graphics.Matrix doesn't support fill scale types. The workaround is - // mapping inversely from destination to source, then invert the matrix. - matrix.setRectToRect(destination, source, matrixScaleType) - matrix.invert(matrix) - } - - private fun getViewFinderViewportRectForMismatchedAspectRatios( - transformationInfo: SurfaceRequest.TransformationInfo, - viewFinderSize: Size - ): RectF { - val viewFinderRect = - RectF( - 0f, - 0f, - viewFinderSize.width.toFloat(), - viewFinderSize.height.toFloat() - ) - val rotatedViewportSize = getRotatedViewportSize(transformationInfo) - val rotatedViewportRect = - RectF( - 0f, - 0f, - rotatedViewportSize.width.toFloat(), - rotatedViewportSize.height.toFloat() - ) - val matrix = Matrix() - setMatrixRectToRect( - matrix, - rotatedViewportRect, - viewFinderRect - ) - matrix.mapRect(rotatedViewportRect) - return rotatedViewportRect - } - - @SuppressLint("RestrictedApi") - fun getSurfaceToViewFinderMatrix( - viewFinderSize: Size, - transformationInfo: SurfaceRequest.TransformationInfo, - isFrontCamera: Boolean - ): Matrix { - // Get the target of the mapping, the coordinates of the crop rect in view finder. - val viewFinderCropRect: RectF = - if (isViewportAspectRatioMatchViewFinder(transformationInfo, viewFinderSize)) { - // If crop rect has the same aspect ratio as view finder, scale the crop rect to - // fill the entire view finder. This happens if the scale type is FILL_* AND a - // view-finder-based viewport is used. - RectF( - 0f, - 0f, - viewFinderSize.width.toFloat(), - viewFinderSize.height.toFloat() - ) - } else { - // If the aspect ratios don't match, it could be 1) scale type is FIT_*, 2) the - // Viewport is not based on the view finder or 3) both. - getViewFinderViewportRectForMismatchedAspectRatios( - transformationInfo, - viewFinderSize - ) - } - val matrix = - TransformUtils.getRectToRect( - RectF(transformationInfo.cropRect), - viewFinderCropRect, - transformationInfo.rotationDegrees - ) - if (isFrontCamera && transformationInfo.hasCameraTransform()) { - // SurfaceView/TextureView automatically mirrors the Surface for front camera, which - // needs to be compensated by mirroring the Surface around the upright direction of the - // output image. This is only necessary if the stream has camera transform. - // Otherwise, an internal GL processor would have mirrored it already. - if (TransformUtils.is90or270(transformationInfo.rotationDegrees)) { - // If the rotation is 90/270, the Surface should be flipped vertically. - // +---+ 90 +---+ 270 +---+ - // | ^ | --> | < | | > | - // +---+ +---+ +---+ - matrix.preScale( - 1f, - -1f, - transformationInfo.cropRect.centerX().toFloat(), - transformationInfo.cropRect.centerY().toFloat() - ) - } else { - // If the rotation is 0/180, the Surface should be flipped horizontally. - // +---+ 0 +---+ 180 +---+ - // | ^ | --> | ^ | | v | - // +---+ +---+ +---+ - matrix.preScale( - -1f, - 1f, - transformationInfo.cropRect.centerX().toFloat(), - transformationInfo.cropRect.centerY().toFloat() - ) - } - } - return matrix - } - - fun getTransformedSurfaceRect( - resolution: Size, - transformationInfo: SurfaceRequest.TransformationInfo, - viewFinderSize: Size, - isFrontCamera: Boolean - ): RectF { - val surfaceToViewFinder: Matrix = - getSurfaceToViewFinderMatrix( - viewFinderSize, - transformationInfo, - isFrontCamera - ) - val rect = RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat()) - surfaceToViewFinder.mapRect(rect) - return rect - } -} diff --git a/domain/camera/build.gradle.kts b/domain/camera/build.gradle.kts index 670fd8f7f..86552ba45 100644 --- a/domain/camera/build.gradle.kts +++ b/domain/camera/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2023-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. @@ -68,7 +68,6 @@ dependencies { implementation(libs.camera.lifecycle) implementation(libs.camera.video) - implementation(libs.camera.view) implementation(libs.camera.extensions) // Hilt diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts index 126fe88c2..e4baf8d1a 100644 --- a/feature/preview/build.gradle.kts +++ b/feature/preview/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2023-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. @@ -101,7 +101,7 @@ dependencies { // CameraX implementation(libs.camera.core) - implementation(libs.camera.view) + implementation(libs.camera.viewfinder.compose) // Hilt implementation(libs.dagger.hilt.android) @@ -113,7 +113,6 @@ dependencies { // Project dependencies implementation(project(":data:settings")) implementation(project(":domain:camera")) - implementation(project(":camera-viewfinder-compose")) implementation(project(":feature:quicksettings")) } diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraXViewfinder.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraXViewfinder.kt new file mode 100644 index 000000000..7a8fa6842 --- /dev/null +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraXViewfinder.kt @@ -0,0 +1,128 @@ +/* + * 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.feature.preview.ui + +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. + */ +@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/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt index 01f09c8ee..59c64c34b 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2023-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. @@ -17,7 +17,6 @@ package com.google.jetpackcamera.feature.preview.ui import android.util.Log import android.view.Display -import android.view.View import android.widget.Toast import androidx.camera.core.Preview import androidx.compose.animation.core.animateFloatAsState @@ -63,7 +62,6 @@ import com.google.jetpackcamera.feature.preview.VideoRecordingState import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.Stabilization import com.google.jetpackcamera.settings.model.SupportedStabilizationMode -import com.google.jetpackcamera.viewfinder.CameraPreview import kotlinx.coroutines.CompletableDeferred private const val TAG = "PreviewScreen" @@ -123,7 +121,6 @@ fun PreviewDisplay( Log.d(TAG, "onSurfaceProviderReady") deferredSurfaceProvider.complete(it) } - lateinit var viewInfo: View BoxWithConstraints( Modifier @@ -135,22 +132,6 @@ fun PreviewDisplay( // double tap to flip camera Log.d(TAG, "onDoubleTap $offset") onFlipCamera() - }, - onTap = { offset -> - // tap to focus - try { - onTapToFocus( - viewInfo.display, - viewInfo.width, - viewInfo.height, - offset.x, - offset.y - ) - Log.d(TAG, "onTap $offset") - } catch (e: UninitializedPropertyAccessException) { - Log.d(TAG, "onTap $offset") - e.printStackTrace() - } } ) }, @@ -169,16 +150,10 @@ fun PreviewDisplay( .transformable(state = transformableState) ) { - CameraPreview( + CameraXViewfinder( modifier = Modifier .fillMaxSize(), - onSurfaceProviderReady = onSurfaceProviderReady, - onRequestBitmapReady = { - it.invoke() - }, - setSurfaceView = { s: View -> - viewInfo = s - } + onSurfaceProviderReady = onSurfaceProviderReady ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e867155ab..121ca15f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,9 @@ androidxCoreKtx = "1.12.0" androidxTracing = "1.2.0" benchmarkMacroJunit4 = "1.2.2" camerax = "1.4.0-SNAPSHOT" +camerax-viewfinder-compose = "1.0.0-SNAPSHOT" compose = "1.5.4" -composeBom = "2023.10.01" +composeBom = "2024.02.00" composeMaterial3 = "1.1.2" coreKtx = "1.5.0" coroutinesCore = "1.7.1" @@ -58,7 +59,7 @@ camera-core = { module = "androidx.camera:camera-core", version.ref = "camerax" camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" } camera-video = { module = "androidx.camera:camera-video", version.ref = "camerax" } -camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" } +camera-viewfinder-compose = { module = "androidx.camera:camera-viewfinder-compose", version.ref = "camerax-viewfinder-compose" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial3" } diff --git a/settings.gradle.kts b/settings.gradle.kts index b611e8d81..22041cbb5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,7 +25,7 @@ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { - setUrl("https://androidx.dev/snapshots/builds/11359450/artifacts/repository") + setUrl("https://androidx.dev/snapshots/builds/11426020/artifacts/repository") } google() mavenCentral() @@ -35,7 +35,6 @@ rootProject.name = "Jetpack Camera" include(":app") include(":feature:preview") include(":domain:camera") -include(":camera-viewfinder-compose") include(":feature:settings") include(":data:settings") include(":core:common")