You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中CameraX预览异常与重组合屏闪问题求助

Jetpack Compose相机界面问题修复方案

问题描述

在Jetpack Compose中开发相机界面,包含SubcomposeAsyncImage组件用于拍摄后同步更新图片计数,当前存在两个问题:

  • 相机预览无法完全显示:拍照前底部存在黑块,拍摄后预览才全屏显示;
  • SubcomposeAsyncImage更新时,CameraX预览触发重组合导致屏幕闪屏。

问题1:相机预览底部黑块修复

原因分析

CameraX的PreviewView默认采用AspectRatio.RATIO_4_3,若设备屏幕为16:9等其他比例,会出现预览无法填充全屏的黑块;同时Scaffold的内边距直接应用到预览容器,进一步挤压了预览空间。

修复代码

@Composable
fun CameraScreenPreview(
    onPhotoSaved: (File) -> Unit,
    stackUrl: String,
    stackCount: Int
) {
    val haptic = LocalHapticFeedback.current
    val context = LocalContext.current
    val controller = remember {
        LifecycleCameraController(context).apply {
            setEnabledUseCases(CameraController.IMAGE_CAPTURE)
            // 适配屏幕宽高比
            cameraInfo?.let { info ->
                val screenRatio = if (context.resources.displayMetrics.widthPixels > context.resources.displayMetrics.heightPixels) {
                    AspectRatio.RATIO_16_9
                } else {
                    AspectRatio.RATIO_9_16
                }
                setTargetAspectRatio(Preview.SurfaceRequest.getAspectRatio(info, screenRatio))
            }
        }
    }

    Scaffold { padding ->
        Box(modifier = Modifier.fillMaxSize()) {
            CameraPreview(controller)

            if (stackUrl.isNotEmpty()) {
                Stack(
                    url = stackUrl,
                    stackCount = stackCount,
                    modifier = Modifier
                        .padding(start = 16.dp, bottom = padding.calculateBottomPadding() + 16.dp)
                        .align(Alignment.BottomStart)
                )
            }

            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = padding.calculateBottomPadding() + 16.dp)
                    .align(Alignment.BottomCenter),
                horizontalArrangement = Arrangement.SpaceAround
            ) {
                IconButton(onClick = {
                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)
                    takePhoto(controller, context, onPhotoSaved) {}
                }, modifier = Modifier.size(80.dp)) {
                    Icon(
                        painter = painterResource(id = R.drawable.camera),
                        contentDescription = stringResource(R.string.take_photo),
                        tint = Color.Unspecified
                    )
                }
            }
        }
    }
}

@Composable
fun CameraPreview(controller: LifecycleCameraController) {
    val lifecycleOwner = LocalLifecycleOwner.current
    AndroidView(
        factory = {
            PreviewView(it).apply {
                this.controller = controller
                controller.bindToLifecycle(lifecycleOwner)
                // 设置缩放模式填充屏幕
                scaleType = PreviewView.ScaleType.FILL_CENTER
            }
        },
        modifier = Modifier.fillMaxSize()
    )
}

问题2:SubcomposeAsyncImage更新导致预览闪屏修复

原因分析

CameraScreenPreview直接依赖stackUrl和stackCount作为参数,这两个值更新时会触发整个组件重组合,导致CameraPreview中的AndroidView重新创建,引发闪屏。

修复代码

ViewModel优化

@HiltViewModel
open class CamViewModel @Inject constructor() : ViewModel() {
    private val _docs = MutableStateFlow<List<String>>(emptyList())
    val docs = _docs.asStateFlow()

    // 通过StateFlow派生计算栈状态,避免冗余状态更新
    val stackState = docs.map { docsList ->
        Pair(docsList.lastOrNull() ?: "", docsList.size)
    }.stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(5000),
        Pair("", 0)
    )

    fun onTakePhoto(file: File) {
        _docs.update { it + file.path }
    }
}

界面组件修改

@Composable
fun CamScreen(
    navController: NavController, viewModel: CamViewModel = hiltViewModel()
) {
    val stackState by viewModel.stackState.collectAsState()
    CameraContent(
        onTakePhoto = viewModel::onTakePhoto,
        stackUrl = stackState.first,
        stackCount = stackState.second
    )
}

@Composable
fun CameraScreenPreview(
    onPhotoSaved: (File) -> Unit,
    stackUrl: String,
    stackCount: Int
) {
    val haptic = LocalHapticFeedback.current
    val context = LocalContext.current
    // 使用remember保持Controller实例稳定,避免重组合时重建
    val controller = remember(context) {
        LifecycleCameraController(context).apply {
            setEnabledUseCases(CameraController.IMAGE_CAPTURE)
            cameraInfo?.let { info ->
                val screenRatio = if (context.resources.displayMetrics.widthPixels > context.resources.displayMetrics.heightPixels) {
                    AspectRatio.RATIO_16_9
                } else {
                    AspectRatio.RATIO_9_16
                }
                setTargetAspectRatio(Preview.SurfaceRequest.getAspectRatio(info, screenRatio))
            }
        }
    }

    Scaffold { padding ->
        Box(modifier = Modifier.fillMaxSize()) {
            // CameraPreview不依赖stack参数,仅依赖稳定的controller,不会因栈更新重组合
            CameraPreview(controller)

            if (stackUrl.isNotEmpty()) {
                Stack(
                    url = stackUrl,
                    stackCount = stackCount,
                    modifier = Modifier
                        .padding(start = 16.dp, bottom = padding.calculateBottomPadding() + 16.dp)
                        .align(Alignment.BottomStart)
                )
            }

            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = padding.calculateBottomPadding() + 16.dp)
                    .align(Alignment.BottomCenter),
                horizontalArrangement = Arrangement.SpaceAround
            ) {
                IconButton(onClick = {
                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)
                    takePhoto(controller, context, onPhotoSaved) {}
                }, modifier = Modifier.size(80.dp)) {
                    Icon(
                        painter = painterResource(id = R.drawable.camera),
                        contentDescription = stringResource(R.string.take_photo),
                        tint = Color.Unspecified
                    )
                }
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者hushed_voice

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 09:12:07