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
相关产品推荐
相关产品推荐

