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")