如何用CameraX+Compose实现Telegram式相机打开过渡效果?
实现Telegram同款相机全屏过渡效果(Compose版本)
核心思路是将Camera容器从LazyGrid的item中剥离,放到父布局层级,通过尺寸、位置动画实现从网格项到全屏的过渡,同时避免Camera实例因Composable重组而销毁重建。
具体实现步骤
1. 独立的Camera容器Composable
这个容器负责承载TextureView和相机逻辑,通过动画控制自身的尺寸、位置和圆角,不会随LazyGrid的item销毁而重建:
@Composable fun CameraContainer( isFullscreen: Boolean, gridItemScreenBounds: Rect, onTextureViewReady: (TextureView) -> Unit ) { val density = LocalDensity.current // 计算目标尺寸:全屏时用屏幕大小,非全屏时用网格项的尺寸 val targetSize = if (isFullscreen) { val config = LocalConfiguration.current with(density) { Size(config.screenWidthDp.dp.toPx(), config.screenHeightDp.dp.toPx()) } } else { Size(gridItemScreenBounds.width(), gridItemScreenBounds.height()) } // 计算目标偏移:全屏时归位到左上角,非全屏时回到网格项的屏幕坐标 val targetOffset = if (isFullscreen) { Offset(0f, 0f) } else { Offset(gridItemScreenBounds.left, gridItemScreenBounds.top) } // 尺寸和偏移的动画 val animatedSize by animateSizeAsState(targetSize, animationSpec = tween(300, easing = FastOutSlowInEasing)) val animatedOffset by animateOffsetAsState(targetOffset, animationSpec = tween(300, easing = FastOutSlowInEasing)) val animatedCornerRadius by animateDpAsState(if (isFullscreen) 0.dp else 12.dp, animationSpec = tween(300)) Box( modifier = Modifier .offset { IntOffset(animatedOffset.x.roundToInt(), animatedOffset.y.roundToInt()) } .size( width = with(density) { animatedSize.width.toDp() }, height = with(density) { animatedSize.height.toDp() } ) .clip(RoundedCornerShape(animatedCornerRadius)) .zIndex(1f) // 确保在LazyGrid上层 ) { AndroidView( factory = { context -> TextureView(context).apply { onTextureViewReady(this) } }, modifier = Modifier.fillMaxSize() ) } }
2. 主布局整合LazyGrid与Camera容器
在主布局中,LazyGrid的相机项仅作为占位入口,点击后触发Camera容器的全屏动画:
@Composable fun CameraGridScreen() { var isCameraFullscreen by remember { mutableStateOf(false) } var cameraGridItemBounds by remember { mutableStateOf(Rect.Zero) } var cameraTextureView by remember { mutableStateOf<TextureView?>(null) } var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) } val lifecycleOwner = LocalLifecycleOwner.current // 管理相机生命周期:仅当容器需要显示时初始化,关闭后释放 LaunchedEffect(cameraTextureView, isCameraFullscreen) { cameraTextureView?.let { textureView -> val shouldInit = isCameraFullscreen || cameraGridItemBounds != Rect.Zero if (shouldInit) { val provider = ProcessCameraProvider.getInstance(textureView.context).get() cameraProvider = provider val preview = Preview.Builder().build().also { it.setSurfaceProvider(textureView.surfaceProvider) } val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA try { provider.unbindAll() provider.bindToLifecycle(lifecycleOwner, cameraSelector, preview) } catch (e: Exception) { // 处理相机绑定异常 } } else { cameraProvider?.unbindAll() cameraProvider = null } } } Box(modifier = Modifier.fillMaxSize()) { // 图片网格 LazyVerticalGrid(columns = GridCells.Fixed(3)) { items(9) { index -> if (index == 4) { // 中间项作为相机入口 Box( modifier = Modifier .fillMaxSize() .padding(4.dp) .onGloballyPositioned { coords -> // 获取网格项在屏幕中的边界 cameraGridItemBounds = coords.boundsInWindow() } .clickable { isCameraFullscreen = true } ) { // 相机占位图标 Icon( Icons.Filled.Camera, contentDescription = "打开相机", modifier = Modifier.align(Alignment.Center), tint = Color.Gray ) } } else { // 其他网格项示例 Box( modifier = Modifier .fillMaxSize() .padding(4.dp) .background(Color.LightGray) ) } } } // 相机容器:仅当需要显示时渲染 if (isCameraFullscreen || cameraGridItemBounds != Rect.Zero) { CameraContainer( isFullscreen = isCameraFullscreen, gridItemScreenBounds = cameraGridItemBounds, onTextureViewReady = { cameraTextureView = it } ) } // 全屏状态下的关闭按钮 if (isCameraFullscreen) { IconButton( onClick = { isCameraFullscreen = false }, modifier = Modifier .align(Alignment.TopEnd) .padding(16.dp) .background(Color.Black.copy(alpha = 0.5f), CircleShape) ) { Icon(Icons.Filled.Close, contentDescription = "关闭相机", tint = Color.White) } } } }
关键细节说明
- 避免Camera重建:Camera容器脱离LazyGrid的item层级,不会因网格滚动或重组被销毁,确保Camera实例在过渡过程中持续存在。
- 动画平滑过渡:通过
animateSizeAsState、animateOffsetAsState实现尺寸和位置的连续动画,匹配Telegram的过渡曲线。 - 生命周期管理:利用
LaunchedEffect监听容器显示状态,仅在需要时初始化相机,关闭后及时释放资源。 - 权限处理:在实际项目中,需确保在初始化相机前已获取
CAMERA权限,可配合rememberLauncherForActivityResult实现权限申请逻辑。
内容的提问来源于stack exchange,提问作者Armen Sarkisyan
相关产品推荐
相关产品推荐

