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