稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Compose CameraX现已稳定:给Composer的端到端指南

本文译自「Compose-Native CameraX Is Now Stable: End-to-End Guide for Jetpack Compose」,原文链接https://proandroiddev.com/goodbye-androidview-camerax-goes-full-compose-4d21ca234c4e,由Ioannis Anifantakis发布于20251026。

简介

还记得你在 Jetpack Compose 中的第一个相机页面吗?纯粹的声明式乐趣……直到预览。然后是熟悉的 AndroidView(PreviewView) 绕道。它确实有效,但总感觉不对:composables中间有一个 View 形状的空洞(类似于 _iFrame__ ……),而且点击对焦的数学计算总是让人感觉不太可靠。

在 I/O 25 之后,这种妥协已经结束。

  • 不再 使用 **AndroidView(PreviewView)** 进行相机预览。
  • 新增 **CameraXViewfinder** 可组合项,可在 Compose 中直接渲染 CameraX SurfaceRequest
  • 修正了内置坐标变换(点击对焦、叠加层),并建立了更简洁、更具声明性的心智模型。

注意:

在 I/O 25 大会上,Compose 支持已发布 alpha/beta 版本,稳定版已于 9 月发布——现在是时候了解一下了。”_

配套项目

你可以在 GitHub 上的配套项目 找到本文的配套项目,该项目演示了 CameraX 中 Jetpack Compose 的新功能。

权限用户体验(简要说明)

本文将重点介绍 Compose + CameraX 的功能。配套项目 实现了完整的运行时流程:

  • 在预览入口点请求 **CAMERA**
  • 仅在用户开始录制时(按需麦克风)请求 **RECORD_AUDIO**
  • 一个小型的 PermissionGate 可组合函数负责处理 Compose 树中的授权/拒绝/重新请求。
  • 为了满足 Lint 对 @RequiresPermission 的要求,调用点还会在调用与麦克风相关的 API 之前执行显式 checkSelfPermission(...)

请参阅代码库,了解具体的 PermissionGate 以及我们如何将其连接到 Capture 页面。

实际变化是什么?

CameraX 团队放弃了 androidx.camera:camera-compose,取而代之的是看似简单的 API:**CameraXViewfinder**。但这不仅仅是“将 PreviewView 封装在可组合项中”。这是对 Compose 的彻底重写,也是对相机 Surface 与 Compose 集成方式的根本性重新思考。

以下是架构层面的变化:

Compose 目标优先 取景器渲染管道现在将 Compose 视为主要平台。Surface 生命周期、旋转处理和缩放都以 Compose 惯用的方式进行。

开箱即用的正确坐标变换 还记得计算预览中的点击实际映射到相机传感器的位置,并考虑旋转、宽高比裁剪和缩放模式吗?MutableCoordinateTransformer 可以处理这些。点击对焦现在……可以正常工作了。

真正的可组合语义 想要将预览 clip() 转换为自定义形状?应用 graphicsLayer 变换?使用 AnimatedContent 为其添加动画效果?现在,你可以轻松完成所有这些操作,而无需与渲染器冲突。它与其他可组合组件一样。

CameraX 1.5.x 成熟度 整个技术栈都得到了完善:适用于 Kotlin 协程的 ProcessCameraProvider.awaitInstance()、全面稳定的构件以及更完善的文档。这并非 Beta 测试……它已准备好投入生产。

为什么这真的很重要

如果你一直在构建相机功能,你就会知道其中的痛点

  • 心智模型分裂:“用 Compose 思考 UI,用 View 思考相机,并在两者之间不断转换。”
  • 手势协调的噩梦:在 Compose 中处理触摸事件,在 View 坐标系中测光对焦,祈祷你的计算准确无误。
  • Z 轴顺序难题:“PreviewView” 经常使用在单独图层中渲染的“SurfaceView”。Compose 叠加层无法可靠地位于顶部,因此十字线、参考线和按钮可能会消失在预览层后面。
  • 生命周期之舞:使用 CameraX 用例绑定将 Compose 重组与 View 生命周期同步

所有这些摩擦?都消失了。

“现在,你可以像编写其他现代 Android 应用一样编写相机 UI。一个范例。一个心智模型。纯粹的 Compose。”

代码演示

让我们从最基本的开始——一个可以工作的相机预览(固定状态模式:将写入器 MutableStateFlow读取器 collectAsState 分离)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Composable
fun CameraPreview(modifier: Modifier = Modifier) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    // Writer: MutableStateFlow we can update from CameraX callbacks
    val surfaceRequests = remember { MutableStateFlow<SurfaceRequest?>(null) }

    // Reader: Compose state derived from the flow
    val surfaceRequest by surfaceRequests.collectAsState(initial = null)

    // Bind CameraX use cases once
    LaunchedEffect(Unit) {
        val provider = ProcessCameraProvider.awaitInstance(context)

        val preview = Preview.Builder().build().apply {
            // When CameraX needs a surface, publish it to Compose
            setSurfaceProvider { request ->
                surfaceRequests.value = request
            }
        }

        provider.unbindAll()
        provider.bindToLifecycle(
            lifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            preview
        )
    }

    // The actual Compose viewfinder
    surfaceRequest?.let { request ->
        CameraXViewfinder(
            surfaceRequest = request,
            modifier = modifier.fillMaxSize()
        )
    }
}

就是这样。没有 AndroidView。没有 PreviewView。只有一个可组合组件,它接收 SurfaceRequest 并进行渲染。

模式很简洁:“CameraX 发布 Surface 请求,Compose 处理它们。单向。没有回调在各个世界之间来回切换。”

可选:使用镜头切换按钮(FAB)进行预览(前/后)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Composable
fun PreviewWithLensSwitch(modifier: Modifier = Modifier) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    val surfaceRequests = remember { MutableStateFlow<SurfaceRequest?>(null) }
    val surfaceRequest by surfaceRequests.collectAsState(initial = null)

    // remember current lens
    var useFront by rememberSaveable { mutableStateOf(false) }
    val selector = if (useFront) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA

    // bind when camera selector changes (front/back camera)
    LaunchedEffect(selector) {
        val provider = ProcessCameraProvider.awaitInstance(context)
        val preview = Preview.Builder().build().apply {
            setSurfaceProvider { req -> surfaceRequests.value = req }
        }
        provider.unbindAll()
        provider.bindToLifecycle(lifecycleOwner, selector, preview)
    }

    Box(Modifier.fillMaxSize()) {
        surfaceRequest?.let { req ->
            CameraXViewfinder(surfaceRequest = req, modifier = Modifier.fillMaxSize())
        }
        FloatingActionButton(
            onClick = { useFront = !useFront },
            modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp)
        ) { Icon(Icons.Rounded.Cameraswitch, contentDescription = "Switch camera") }
    }
}

真正的考验:交互式相机控件

旧方法的失败之处就在这里。让我们实现点击对焦和捏合缩放……这些功能过去需要对视图坐标系进行一些 hack(同样使用固定的写入/读取模式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@Composable
fun InteractiveCameraPreview(

modifier: Modifier = Modifier,

onFocusTap: (success: Boolean) -> Unit = {}) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    var camera by remember { mutableStateOf<Camera?>(null) }

    val surfaceRequests = remember { MutableStateFlow<SurfaceRequest?>(null) }
    val surfaceRequest by surfaceRequests.collectAsState(initial = null)

    // Bind camera once
    LaunchedEffect(Unit) {
        val provider = ProcessCameraProvider.awaitInstance(context)
        val preview = Preview.Builder().build().apply {
            setSurfaceProvider { req -> surfaceRequests.value = req }
        }

        camera = provider.bindToLifecycle(
            lifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            preview
        )
    }

    // Coordinate transformer: Compose UI → Camera surface
    val coordinateTransformer = remember { MutableCoordinateTransformer() }

    surfaceRequest?.let { request ->
        CameraXViewfinder(
            surfaceRequest = request,
            coordinateTransformer = coordinateTransformer,
            modifier = modifier
                .fillMaxSize()
                .pointerInput(camera) {
                    // Tap-to-focus
                    detectTapGestures { offset ->
                        val cam = camera ?: return@detectTapGestures

                        // Transform Compose coordinates to camera surface
                        val surfacePoint = with(coordinateTransformer) {
                            offset.transform()
                        }

                        val meteringFactory = SurfaceOrientedMeteringPointFactory(
                            request.resolution.width.toFloat(),
                            request.resolution.height.toFloat()
                        )

                        val focusPoint = meteringFactory.createPoint(
                            surfacePoint.x,
                            surfacePoint.y
                        )

                        val action = FocusMeteringAction.Builder(
                            focusPoint,
                            FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE
                        ).setAutoCancelDuration(3, TimeUnit.SECONDS).build()

                        cam.cameraControl
                            .startFocusAndMetering(action)
                            .addListener(
                                { onFocusTap(true) },
                                ContextCompat.getMainExecutor(context)
                            )
                    }
                }
                .pointerInput(camera) {
                    // Pinch-to-zoom
                    detectTransformGestures { _, _, zoom, _ ->
                        val cam = camera ?: return@detectTransformGestures
                        val zoomState = cam.cameraInfo.zoomState.value ?: return@detectTransformGestures

                        val newRatio = (zoomState.zoomRatio * zoom).coerceIn(
                            zoomState.minZoomRatio,
                            zoomState.maxZoomRatio
                        )

                        cam.cameraControl.setZoomRatio(newRatio)
                    }
                }
        )
    }
}

看看这个点击对焦的实现。注意你没有做的事情:

  • 无需手动旋转补偿
  • 无需进行坐标映射的宽高比计算
  • 无需进行视图 → 表面 → 传感器坐标链计算
  • 无需进行“祈祷它在横向模式下能正常工作”的漫长测试

MutableCoordinateTransformer 可以处理所有这些。你点击 Compose 坐标系,它会转换为相机坐标系,就完成了。

这就是“技术上可行”和“实际易于实现”之间的区别。

拍摄照片和视频

添加拍摄功能遵循相同的简洁模式——绑定其他用例,并从 Compose 界面触发它们。

我们还将仅在尝试录制时请求麦克风,并使用简单的“PermissionGate”模式(与我们项目在需要时仅请求音频的方法一致)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
@Composable
fun CameraScreen() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    var camera by remember { mutableStateOf<Camera?>(null) }
    var imageCapture by remember { mutableStateOf<ImageCapture?>(null) }
    var videoCapture by remember { mutableStateOf<VideoCapture<Recorder>?>(null) }
    var activeRecording by remember { mutableStateOf<Recording?>(null) }

    val surfaceRequests = remember { MutableStateFlow<SurfaceRequest?>(null) }
    val surfaceRequest by surfaceRequests.collectAsState(initial = null)

    // Bind all use cases
    LaunchedEffect(Unit) {
        val provider = ProcessCameraProvider.awaitInstance(context)

        val preview = Preview.Builder().build().apply {
            setSurfaceProvider { req -> surfaceRequests.value = req }
        }

        imageCapture = ImageCapture.Builder()
            .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
            .build()

            val recorder = Recorder.Builder()
            .setQualitySelector(QualitySelector.from(Quality.FHD))
            .build()
        videoCapture = VideoCapture.withOutput(recorder)

        camera = provider.bindToLifecycle(
            lifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            preview,
            imageCapture!!,
            videoCapture!!
        )
    }

    Box(modifier = Modifier.fillMaxSize()) {
        // Camera preview
        surfaceRequest?.let { request ->
            CameraXViewfinder(
                surfaceRequest = request,
                modifier = Modifier.fillMaxSize()
            )
        }

        // Compose UI controls
        Row(
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(bottom = 32.dp)
        ) {
            // Capture photo button
            IconButton(
                onClick = { capturePhoto(context, imageCapture) }
            ) {
                Icon(Icons.Default.PhotoCamera, "Take Photo")
            }

            Spacer(modifier = Modifier.width(32.dp))

            // Video record toggle (mic requested only when needed)
            PermissionGate(
                permission = Permission.RECORD_AUDIO,
                // Optional: custom UI if permission is not yet granted
                contentNonGranted = { missing, humanReadable, requestPermissions ->
                    // Minimal, inline UX: re-request directly
                    Button(onClick = { requestPermissions(missing) }) {
                        Text("Grant $humanReadable")
                    }
                }
            ) {
                IconButton(
                    onClick = {
                        activeRecording = toggleRecording(
                            context,
                            videoCapture,
                            activeRecording
                        )
                    }
                ) {
                    Icon(
                        if (activeRecording == null) Icons.Default.RadioButtonUnchecked
                        else Icons.Default.Stop,
                        "Record Video"
                    )
                }
            }
        }
    }
}

private fun capturePhoto(context: Context, imageCapture: ImageCapture?) {
    val capture = imageCapture ?: return

    val name = "IMG_${System.currentTimeMillis()}.jpg"
    val contentValues = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, name)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        // On Android 10+ you could also set RELATIVE_PATH = "DCIM/CameraX"
    }

    val outputOptions = ImageCapture.OutputFileOptions.Builder(
        context.contentResolver,
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        contentValues
    ).build()

    capture.takePicture(
        outputOptions,
        ContextCompat.getMainExecutor(context),
        object : ImageCapture.OnImageSavedCallback {
            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                // Success: output.savedUri
            }
            override fun onError(exception: ImageCaptureException) {
                // Handle error
            }
        }
    )
}

private fun toggleRecording(
    context: Context,
    videoCapture: VideoCapture<Recorder>?,
    currentRecording: Recording?
): Recording? {
    val capture = videoCapture ?: return null

    // Stop if already recording
    if (currentRecording != null) {
        currentRecording.stop()
        return null
    }

    // Start new recording
    val name = "VID_${System.currentTimeMillis()}.mp4"
    val contentValues = ContentValues().apply {
        put(MediaStore.Video.Media.DISPLAY_NAME, name)
        // On Android 10+ you could also set RELATIVE_PATH = "DCIM/CameraX"
    }

    val outputOptions = MediaStoreOutputOptions.Builder(
        context.contentResolver,
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
    ).setContentValues(contentValues).build()

    return capture.output
        .prepareRecording(context, outputOptions)
        .withAudioEnabled() // mic permission is ensured by PermissionGate above
        .start(ContextCompat.getMainExecutor(context)) { event ->
            // Handle recording events (e.g., finalize, error)
        }
}

这是纯粹的 Compose UI 构建。你的相机按钮与预览位于同一个可组合树中。没有桥接逻辑。无需管理单独的 View 层次结构。

迁移策略:PreviewView → CameraXViewfinder

如果你现有的相机代码使用“PreviewView”,则迁移路径如下:

迁移前(旧方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Composable
fun OldCameraPreview() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val previewView = remember { PreviewView(context) }

    LaunchedEffect(previewView) {
        val provider = ProcessCameraProvider.getInstance(context).get()
        val preview = Preview.Builder().build()
        preview.setSurfaceProvider(previewView.surfaceProvider)
        provider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview)
    }

    AndroidView(
        factory = { previewView },
        modifier = Modifier.fillMaxSize()
    )
}

迁移后(Compose 原生方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Composable
fun NewCameraPreview() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val selector = CameraSelector.DEFAULT_BACK_CAMERA

    val surfaceRequests = remember { MutableStateFlow<SurfaceRequest?>(null) }
    val surfaceRequest by surfaceRequests.collectAsState(initial = null)

    LaunchedEffect(Unit) {
        val provider = ProcessCameraProvider.awaitInstance(context)
        val preview = Preview.Builder().build().apply {
            setSurfaceProvider { req -> surfaceRequests.value = req }
        }
        provider.unbindAll()
        provider.bindToLifecycle(lifecycleOwner, selector, preview)
    }

    surfaceRequest?.let {
        CameraXViewfinder(
            surfaceRequest = it,
            modifier = Modifier.fillMaxSize()
        )
    }
}

关键的思维转变:不再将 View 的“SurfaceProvider”赋予 CameraX,而是将“SurfaceRequest”对象发布到 Compose 状态,并使用“CameraXViewfinder”进行渲染。

所需依赖项

添加到你的 build.gradle.kts 中:

1
2
3
4
5
6
7
8
9
val cameraxVersion = "1.5.1"dependencies {
    implementation("androidx.camera:camera-core:$cameraxVersion")
    implementation("androidx.camera:camera-camera2:$cameraxVersion")
    implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
    implementation("androidx.camera:camera-video:$cameraxVersion")

    // The new Compose-native viewfinder
    implementation("androidx.camera:camera-compose:$cameraxVersion")
}

清单权限:

1
2
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

实现模式(性能 vs. 合成)

CameraXViewfinder 可以通过两种方式渲染预览:

EXTERNAL(SurfaceView 支持)

相机帧在其自己的 Surface 上渲染,由系统在 Compose 绘制通道之外合成。可以想象成“UI 背后的实时视频层”。通常启用硬件叠加 → 最佳性能/延迟。非常适合在标准 UI 后方显示全屏矩形预览。由于它是一个单独的层,因此对相机像素的逐像素效果(复杂的裁剪/模糊)不适用。

  • 优点:延迟更低,GPU 负载更少,非常适合全屏预览/录制。
  • 缺点:不受逐像素界面特效(圆角蒙版/模糊)的影响,不会显示在 Compose 屏幕截图中。

嵌入式(TextureView 支持)

相机帧作为GPU 纹理绘制在 Compose 渲染通道内部——类似于可重绘面板,其行为与其他可组合项类似。你可以获得深度裁剪/蒙版/动画/模糊/Z 轴排序,但代价是 GPU 工作量增加,延迟略高。

  • 优点:行为类似于普通界面;裁剪、Alpha 通道、模糊、特殊形状和复杂 Z 轴排序均正常。
  • 缺点:GPU 工作量增加 → 在繁重的界面或中端设备上,延迟/卡顿风险略高。

经验法则

  • 全屏/高性能 → 外部
  • 特殊构图/特效 → 嵌入式

如果你未指定模式,库将选择一个合理的默认模式。强制使用以下方式:

1
2
3
4
5
6
import androidx.camera.viewfinder.core.ImplementationMode

CameraXViewfinder(
    surfaceRequest = request,
    implementationMode = ImplementationMode.EXTERNAL // or ImplementationMode.EMBEDDED
)

实际操作中的陷阱

坐标变换并非可选 不要将原始 Compose 偏移量传递给测量工厂。务必使用坐标变换器。数学运算看起来很简单,直到你在横屏、可折叠设备或非标准宽高比设备上进行测试。

前置摄像头是镜像的 如果你正在绘制叠加层或处理拍摄的图像,请记住前置摄像头预览默认是镜像的,但拍摄的图像不是。在你的界面/处理逻辑中考虑到这一点。

在真实设备上测试 不同 OEM 的相机行为有所不同。在 Pixel 上完美运行的功能在三星或小米上可能存在问题。在代表性硬件上测试你的关键流程。

权限用户体验 在入口点请求 CAMERA;仅在开始录制时请求 RECORD_AUDIO(这是一种良好做法)。上面的内联 PermissionGate 模式将该逻辑保留在你的 Compose 树中。

高级功能:可折叠和自适应 UI

由于 CameraXViewfinder 只是另一个可组合项,因此可折叠支持非常简单。简单的双窗格布局或全屏布局通常就足够了;如果需要,可以使用 AnimatedContent 来在状态之间添加动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Composable
fun AdaptiveCameraScreen(surfaceRequest: SurfaceRequest?) {
    val expanded = remember { mutableStateOf(false) } // pretend this reflects window size/hinge state

    AnimatedContent(targetState = expanded.value, label = "layout") { isExpanded ->
        if (isExpanded) {
            Row(Modifier.fillMaxSize()) {
                surfaceRequest?.let {
                    CameraXViewfinder(
                        surfaceRequest = it,
                        modifier = Modifier
                            .weight(1f)
                            .aspectRatio(9f / 16f)
                    )
                }
                Box(Modifier.weight(1f)) { /* CameraControls(Modifier.align(Alignment.Center)) */ }
            }
        } else {
            Box(Modifier.fillMaxSize()) {
                surfaceRequest?.let {
                    CameraXViewfinder(
                        surfaceRequest = it,
                        modifier = Modifier.fillMaxSize()
                    )
                }
                /* CameraControls(Modifier.align(Alignment.BottomCenter)) */
            }
        }
    }
}

测试清单(实用)

  • 验证纵向/横向以及 ContentScale.Crop/Fit 模式下的点击对焦精度。
  • 测试缩放限制;确保捏合和程序化缩放过渡流畅。
  • 切换摄像头(前/后)并重新验证变换 + 镜像行为。
  • 导航离开/后退、旋转和处理配置更改;预览应能够恢复且不闪烁。
  • 在对焦/缩放时录制视频;确保没有表面掉落。

全局展望

此版本的重要性不仅在于它带来的功能,还在于它所传递的信息。

多年来,Android 中的相机开发一直感觉像是二等公民。除了相机页面之外,你可以在任何地方使用 Compose 构建现代 UI,而相机页面则需要你勉强才能与 View 进行互操作。虽然 Compose 确实有效,但编写代码时总感觉像是被束缚了一只手。

camera-compose 不仅仅是一个新产物。CameraX 团队曾说过:“Compose 现在是一流的相机开发平台。”

这意味着:

  • 未来的相机功能将在设计时充分考虑 Compose,而不是对其进行改造。
  • 社区将构建以 Compose 为先的相机库和组件。
  • 最佳实践将围绕可组合相机 UI 不断发展。
  • 文档和示例将反映现代 Android 开发。

我们在整个 Android 生态系统中都看到了这种模式——最初以 View 为中心的 API 正在逐渐获得 Compose 原生的对应版本。camera-compose 就是迄今为止最具影响力的例子之一。

你现在应该做什么

如果你正在开发一个新的相机功能: 从一开始就使用 CameraXViewfinder。甚至不需要考虑 PreviewView。它的代码更简洁,思维模型更简单,你以后会感谢自己的。

如果你已经有相机代码:camera-compose 添加到你的依赖项中,并一次迁移一个页面。从最简单的相机 UI(可能是基本的纯预览页面)开始,熟悉新的 API。然后再处理复杂的部分。

如果你正在构建一个库: 现在是时候将 Compose 原生相机组件添加到你的 SDK 中了。开发者正在寻找可组合的相机解决方案,而生态系统已经为他们做好了准备。

延伸阅读

国王已死 / 国王万岁

“AndroidView”相机预览的时代已经结束。如果你要在 2025 年及以后构建相机功能,那么你将使用 Compose 来构建它们。现在终于有了可以正确支持这些功能的工具。

现在,甩掉那个“AndroidView”包装器,编写一些漂亮的相机 UI 吧。

Comments