Skip to content

Commit 11964a2

Browse files
committed
Making things go compose forward to assure there is no changes there
1 parent 69493cb commit 11964a2

16 files changed

Lines changed: 328 additions & 473 deletions

File tree

‎core-camera/build.gradle.kts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,5 @@ dependencies {
7575
api(libs.androidx.camera.lifecycle)
7676
api(libs.androidx.camera.compose)
7777
api(libs.androidx.camera.viewfinder.core)
78-
api(libs.androidx.camera.viewfinder.view)
78+
api(libs.androidx.camera.viewfinder.compose)
7979
}

‎core-camera/src/main/java/com/android/camera/core/camera2/BaseCamera2Controller.kt‎

Lines changed: 94 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,33 +32,49 @@ import android.os.Build
3232
import android.os.Handler
3333
import android.os.HandlerThread
3434
import android.util.Log
35+
import android.util.Size
3536
import android.view.Surface
3637
import androidx.camera.viewfinder.core.TransformationInfo
37-
import androidx.camera.viewfinder.core.ViewfinderSurfaceSession
38+
import androidx.camera.viewfinder.core.ViewfinderSurfaceRequest
3839
import androidx.camera.viewfinder.core.camera2.Camera2TransformationInfo
39-
import androidx.camera.viewfinder.view.ViewfinderView
4040
import androidx.compose.runtime.Stable
41+
import androidx.compose.runtime.getValue
42+
import androidx.compose.runtime.mutableStateOf
43+
import androidx.compose.runtime.setValue
4144
import androidx.compose.ui.geometry.Offset
45+
import kotlinx.coroutines.NonCancellable
46+
import kotlinx.coroutines.awaitCancellation
47+
import kotlinx.coroutines.suspendCancellableCoroutine
48+
import kotlinx.coroutines.withContext
4249
import java.util.concurrent.Executor
50+
import kotlin.coroutines.resume
4351

4452
private const val TAG = "BaseCamera2Controller"
4553

4654
/**
47-
* Captures the Camera2 plumbing shared by every Camera2 sample: a background [HandlerThread],
48-
* camera discovery/open by lens facing, the viewfinder transformation math, tap-to-focus, and
55+
* Captures the Camera2 plumbing shared by every Camera2 sample: a background [HandlerThread], camera
56+
* discovery/open by lens facing, the viewfinder transformation math, tap-to-focus, and
4957
* cross-API-level capture-session creation.
5058
*
51-
* Subclasses implement [onCameraOpened] to build their preview (and any extra targets such as an
52-
* `ImageReader` or `MediaRecorder` surface), and may override [onCameraPrepared] / [onCameraClosed]
53-
* to allocate and release their own resources.
59+
* Preview is Compose-first: the controller exposes a [surfaceRequest] and [transformationInfo] as
60+
* state, and the [Camera2Preview] composable renders them through the Compose `Viewfinder`. When the
61+
* viewfinder hands back a [Surface], [onCameraOpened] is invoked so the subclass can build its
62+
* preview (and any extra targets such as an `ImageReader`). Subclasses may override [onCameraPrepared]
63+
* / [onCameraClosed] to allocate and release their own resources, and [previewSize] to request a
64+
* different viewfinder resolution.
5465
*/
5566
@Stable
5667
abstract class BaseCamera2Controller(
5768
protected val context: Context,
5869
val isFrontCamera: Boolean,
5970
) {
60-
/** The view the camera renders into. Set by the preview composable before [openCamera]. */
61-
var viewfinder: ViewfinderView? = null
71+
/** The viewfinder surface request, published once the camera opens; null while closed. */
72+
var surfaceRequest: ViewfinderSurfaceRequest? by mutableStateOf(null)
73+
private set
74+
75+
/** How the Compose `Viewfinder` should rotate/mirror the preview for the current device. */
76+
var transformationInfo: TransformationInfo by mutableStateOf(TransformationInfo.DEFAULT)
77+
private set
6278

6379
protected val cameraManager: CameraManager =
6480
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
@@ -69,14 +85,17 @@ abstract class BaseCamera2Controller(
6985
protected var cameraDevice: CameraDevice? = null
7086
protected var captureSession: CameraCaptureSession? = null
7187
protected var previewRequestBuilder: CaptureRequest.Builder? = null
72-
protected var surfaceSession: ViewfinderSurfaceSession? = null
7388

7489
protected var cameraId: String = ""
7590
protected var currentCharacteristics: CameraCharacteristics? = null
7691
protected var currentDisplayRotation: Int = Surface.ROTATION_0
7792

93+
private var previewSurface: Surface? = null
7894
private var isCameraOpeningOrOpen: Boolean = false
7995

96+
/** The resolution requested for the preview viewfinder. Subclasses may override. */
97+
protected open val previewSize: Size = Size(1920, 1080)
98+
8099
/** Resolves the camera id matching the requested lens facing, or `null` if none. */
81100
private fun findCameraId(): String? {
82101
val targetFacing =
@@ -92,23 +111,25 @@ abstract class BaseCamera2Controller(
92111
}
93112
}
94113

95-
/** Updates the viewfinder transform so the preview is oriented for the current rotation. */
96-
fun updateTransformationInfo(displayRotation: Int) {
114+
/** Updates the preview transform for the current device display rotation (a `Surface.ROTATION_*`). */
115+
fun updateDisplayRotation(displayRotation: Int) {
97116
currentDisplayRotation = displayRotation
98-
val characteristics = currentCharacteristics ?: return
99-
val currentViewfinder = viewfinder ?: return
117+
recomputeTransformation()
118+
}
100119

120+
private fun recomputeTransformation() {
121+
val characteristics = currentCharacteristics ?: return
101122
val sensorRotation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
102123
val lensFacing =
103124
characteristics.get(CameraCharacteristics.LENS_FACING)
104125
?: CameraCharacteristics.LENS_FACING_BACK
105126
val sign = if (lensFacing == CameraCharacteristics.LENS_FACING_FRONT) 1 else -1
106127

107-
val rotationDegrees = displayRotation.toRotationDegrees()
128+
val rotationDegrees = currentDisplayRotation.toRotationDegrees()
108129
val relativeRotation = (sensorRotation - rotationDegrees * sign + 360) % 360
109130

110131
val baseInfo = Camera2TransformationInfo.createFromCharacteristics(characteristics)
111-
currentViewfinder.transformationInfo =
132+
transformationInfo =
112133
TransformationInfo(
113134
sourceRotation = relativeRotation,
114135
isSourceMirroredHorizontally = baseInfo.isSourceMirroredHorizontally,
@@ -129,14 +150,14 @@ abstract class BaseCamera2Controller(
129150
@SuppressLint("MissingPermission")
130151
private fun openCameraLocked() {
131152
if (cameraDevice != null || isCameraOpeningOrOpen) return
132-
val currentViewfinder = viewfinder ?: return
133153

134154
try {
135155
val id = findCameraId() ?: return
136156
cameraId = id
137157
val characteristics = cameraManager.getCameraCharacteristics(id)
138158
currentCharacteristics = characteristics
139159
onCameraPrepared(characteristics)
160+
recomputeTransformation()
140161
isCameraOpeningOrOpen = true
141162

142163
cameraManager.openCamera(
@@ -145,8 +166,10 @@ abstract class BaseCamera2Controller(
145166
override fun onOpened(camera: CameraDevice) {
146167
isCameraOpeningOrOpen = false
147168
cameraDevice = camera
148-
updateTransformationInfo(currentDisplayRotation)
149-
onCameraOpened(camera, currentViewfinder)
169+
// Publishing the request makes the Compose Viewfinder render and hand us a
170+
// surface (via Camera2Preview -> runViewfinderSession).
171+
surfaceRequest = ViewfinderSurfaceRequest(previewSize.width, previewSize.height)
172+
maybeStartSession()
150173
}
151174

152175
override fun onDisconnected(camera: CameraDevice) {
@@ -171,13 +194,56 @@ abstract class BaseCamera2Controller(
171194
}
172195
}
173196

197+
/**
198+
* Runs the viewfinder surface session, called by [Camera2Preview] from the Compose `Viewfinder`
199+
* once a [surface] is available. Starts the preview session and keeps the surface alive until the
200+
* viewfinder leaves composition, then tears the session down before the surface is released.
201+
*/
202+
suspend fun runViewfinderSession(surface: Surface) {
203+
backgroundHandler.post {
204+
previewSurface = surface
205+
maybeStartSession()
206+
}
207+
try {
208+
awaitCancellation()
209+
} finally {
210+
withContext(NonCancellable) {
211+
suspendCancellableCoroutine { continuation ->
212+
backgroundHandler.post {
213+
try {
214+
captureSession?.close()
215+
} catch (e: Exception) {
216+
Log.w(TAG, "Exception closing capture session", e)
217+
}
218+
captureSession = null
219+
if (previewSurface === surface) previewSurface = null
220+
continuation.resume(Unit)
221+
}
222+
}
223+
}
224+
}
225+
}
226+
227+
/** Starts the preview session once both the device is open and a viewfinder surface exists. */
228+
private fun maybeStartSession() {
229+
val camera = cameraDevice ?: return
230+
val surface = previewSurface ?: return
231+
try {
232+
captureSession?.close()
233+
} catch (e: Exception) {
234+
Log.w(TAG, "Exception closing previous session", e)
235+
}
236+
captureSession = null
237+
onCameraOpened(camera, surface)
238+
}
239+
174240
/** Allocate per-session resources (e.g. an `ImageReader`) before the device is opened. */
175241
protected open fun onCameraPrepared(characteristics: CameraCharacteristics) {}
176242

177-
/** Build and start the preview session once the [camera] device is open. */
243+
/** Build and start the preview session for the open [camera] using the viewfinder [surface]. */
178244
protected abstract fun onCameraOpened(
179245
camera: CameraDevice,
180-
viewfinder: ViewfinderView,
246+
surface: Surface,
181247
)
182248

183249
/** Release per-session resources allocated in [onCameraPrepared]. */
@@ -191,12 +257,16 @@ abstract class BaseCamera2Controller(
191257

192258
private fun closeCameraLocked() {
193259
isCameraOpeningOrOpen = false
194-
captureSession?.close()
260+
try {
261+
captureSession?.close()
262+
} catch (e: Exception) {
263+
Log.w(TAG, "Exception closing capture session", e)
264+
}
195265
captureSession = null
196266
cameraDevice?.close()
197267
cameraDevice = null
198-
surfaceSession?.close()
199-
surfaceSession = null
268+
previewSurface = null
269+
surfaceRequest = null
200270
onCameraClosed()
201271
}
202272

‎core-camera/src/main/java/com/android/camera/core/camera2/Camera2Preview.kt‎

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package com.android.camera.core.camera2
1717

18-
import androidx.camera.viewfinder.view.ViewfinderView
18+
import androidx.camera.viewfinder.compose.Viewfinder
1919
import androidx.compose.foundation.gestures.detectTapGestures
2020
import androidx.compose.foundation.layout.fillMaxSize
2121
import androidx.compose.runtime.Composable
@@ -24,17 +24,17 @@ import androidx.compose.runtime.LaunchedEffect
2424
import androidx.compose.ui.Modifier
2525
import androidx.compose.ui.geometry.Offset
2626
import androidx.compose.ui.input.pointer.pointerInput
27-
import androidx.compose.ui.viewinterop.AndroidView
2827
import androidx.lifecycle.Lifecycle
2928
import androidx.lifecycle.LifecycleEventObserver
3029
import androidx.lifecycle.compose.LocalLifecycleOwner
3130
import com.android.camera.core.display.rememberDisplayRotation
3231

3332
/**
34-
* Renders a [BaseCamera2Controller] preview into a [ViewfinderView] and wires up the boilerplate
35-
* every Camera2 sample needs: display-rotation transforms, lifecycle-driven open/close, and
36-
* tap-to-focus. The owning screen is responsible for creating the controller and calling
37-
* `release()` when it leaves composition.
33+
* Renders a [BaseCamera2Controller]'s preview through the Compose `Viewfinder` (no Android views) and
34+
* wires up the boilerplate every Camera2 sample needs: display-rotation transforms, lifecycle-driven
35+
* open/close, and tap-to-focus. The controller publishes a `surfaceRequest`; this composable feeds
36+
* the resulting surface back via `onSurfaceSession`. The owning screen creates the controller and
37+
* calls `release()` when it leaves composition.
3838
*
3939
* @param onFocusTap invoked with the tapped offset (in view pixels) after focus is requested, so a
4040
* sample can show a focus indicator.
@@ -49,43 +49,15 @@ fun Camera2Preview(
4949
val displayRotation = rememberDisplayRotation()
5050

5151
LaunchedEffect(displayRotation, controller) {
52-
controller.updateTransformationInfo(displayRotation)
52+
controller.updateDisplayRotation(displayRotation)
5353
}
5454

55-
AndroidView(
56-
factory = { ctx ->
57-
ViewfinderView(ctx).apply { controller.viewfinder = this }
58-
},
59-
update = { view ->
60-
controller.viewfinder = view
61-
// If the view is (re)attached while the lifecycle is already RESUMED — e.g. returning to
62-
// the preview from another full-screen state — no ON_RESUME is emitted, so open here as
63-
// well. openCamera() is idempotent, so this is a no-op when the camera is already open.
64-
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
65-
view.post { controller.openCamera() }
66-
}
67-
},
68-
modifier =
69-
modifier
70-
.fillMaxSize()
71-
.pointerInput(controller) {
72-
detectTapGestures(
73-
onTap = { offset ->
74-
controller.focus(offset, size.width.toFloat(), size.height.toFloat())
75-
onFocusTap?.invoke(offset)
76-
},
77-
)
78-
},
79-
)
80-
8155
DisposableEffect(lifecycleOwner, controller) {
8256
val observer =
8357
LifecycleEventObserver { _, event ->
8458
when (event) {
85-
Lifecycle.Event.ON_CREATE,
86-
Lifecycle.Event.ON_RESUME,
87-
-> {
88-
controller.viewfinder?.post { controller.openCamera() }
59+
Lifecycle.Event.ON_CREATE, Lifecycle.Event.ON_RESUME -> {
60+
controller.openCamera()
8961
}
9062

9163
Lifecycle.Event.ON_PAUSE -> {
@@ -101,4 +73,27 @@ fun Camera2Preview(
10173
controller.closeCamera()
10274
}
10375
}
76+
77+
val request = controller.surfaceRequest
78+
if (request != null) {
79+
Viewfinder(
80+
surfaceRequest = request,
81+
transformationInfo = controller.transformationInfo,
82+
modifier =
83+
modifier
84+
.fillMaxSize()
85+
.pointerInput(controller) {
86+
detectTapGestures(
87+
onTap = { offset ->
88+
controller.focus(offset, size.width.toFloat(), size.height.toFloat())
89+
onFocusTap?.invoke(offset)
90+
},
91+
)
92+
},
93+
) {
94+
onSurfaceSession {
95+
controller.runViewfinderSession(surface)
96+
}
97+
}
98+
}
10499
}

‎core-ui/build.gradle.kts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ dependencies {
7070
implementation(libs.ui.tooling.preview)
7171
debugImplementation(libs.ui.tooling)
7272

73-
// Captured-video preview: ExoPlayer + the standard PlayerView UI.
73+
// Captured-video preview: ExoPlayer + the Compose PlayerSurface (no Android views).
7474
implementation(libs.androidx.media3.exoplayer)
75-
implementation(libs.androidx.media3.ui)
75+
implementation(libs.androidx.media3.ui.compose)
7676

7777
// Re-exports :core-camera + :core-theme (and camera libs) to samples.
7878
api(project(":core-camera"))

0 commit comments

Comments
 (0)