如何在Jetpack Compose中实现带视频片段预览的自定义裁剪起止滑块
自定义Jetpack Compose视频裁剪滑块实现方案
我正在开发Jetpack Compose项目,需要实现视频裁剪功能。目前已完成视频裁剪核心逻辑,但在创建可选择视频起止点且带视频片段预览的滑块时遇到问题。曾尝试使用第三方视频编辑器库,通过以下代码在Compose中调用:
DisposableEffect( AndroidView( factory = { context -> VideoEditor(context) } ) )
该库在XML环境中运行正常,但在Jetpack Compose中表现不佳,因此希望在Compose中自定义实现该滑块功能(参考图示:
)。
一、核心组件拆分
- 进度轨道:底部显示视频总时长的进度条,可绘制时间刻度
- 起止控制手柄:左右两个可拖拽的手柄,用于标记裁剪的开始和结束时间
- 预览缩略图条:轨道上方显示视频关键帧缩略图,帮助用户直观判断片段内容
- 实时预览窗口:可选,显示当前选中片段的预览内容
二、关键实现细节
1. 生成视频缩略图
使用MediaMetadataRetriever批量抽取视频关键帧,按固定间隔生成缩略图列表:
fun generateThumbnails(context: Context, videoPath: String, thumbCount: Int): List<Bitmap> { val retriever = MediaMetadataRetriever() val thumbnails = mutableListOf<Bitmap>() try { retriever.setDataSource(videoPath) val totalDuration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0 val interval = totalDuration / thumbCount for (i in 0 until thumbCount) { retriever.getFrameAtTime(i * interval, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)?.let { thumbnails.add(it) } } } catch (e: Exception) { e.printStackTrace() } finally { retriever.release() } return thumbnails }
2. 自定义滑块布局
用Compose的Box和Modifier组合实现轨道、手柄和缩略图的整合:
@Composable fun VideoTrimSlider( thumbnails: List<Bitmap>, totalDuration: Long, startPosition: Float, endPosition: Float, onStartPositionChange: (Float) -> Unit, onEndPositionChange: (Float) -> Unit ) { Box(modifier = Modifier.fillMaxWidth().height(120.dp)) { // 缩略图条:横向排列视频关键帧 Row(modifier = Modifier.fillMaxWidth().align(Alignment.Center)) { thumbnails.forEach { bitmap -> Image( bitmap = bitmap.asImageBitmap(), contentDescription = "视频缩略图", modifier = Modifier.weight(1f).height(80.dp).padding(1.dp) ) } } // 进度轨道:背景+选中区域 Canvas(modifier = Modifier.fillMaxWidth().height(8.dp).align(Alignment.BottomCenter)) { // 绘制灰色背景轨道 drawRoundRect( color = Color.Gray.copy(alpha = 0.6f), cornerRadius = CornerRadius(4.dp.toPx()) ) // 绘制蓝色选中区域 drawRoundRect( color = Color.Blue, topLeft = Offset(startPosition * size.width, 0f), size = Size((endPosition - startPosition) * size.width, size.height), cornerRadius = CornerRadius(4.dp.toPx()) ) } // 开始手柄:可拖拽,限制在0到结束位置之间 Box( modifier = Modifier .offset(x = startPosition * size.width - 20.dp.toPx()) .size(40.dp) .align(Alignment.BottomCenter) .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> val newPos = (startPosition * size.width + delta) / size.width if (newPos in 0f..endPosition) { onStartPositionChange(newPos) } } ) .background(Color.White, CircleShape) .border(2.dp, Color.Blue, CircleShape) ) // 结束手柄:可拖拽,限制在开始位置到1之间 Box( modifier = Modifier .offset(x = endPosition * size.width - 20.dp.toPx()) .size(40.dp) .align(Alignment.BottomCenter) .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> val newPos = (endPosition * size.width + delta) / size.width if (newPos in startPosition..1f) { onEndPositionChange(newPos) } } ) .background(Color.White, CircleShape) .border(2.dp, Color.Blue, CircleShape) ) } }
3. 状态管理与页面整合
用Compose的状态API管理起止位置,结合视频预览组件完成完整页面:
@Composable fun VideoTrimScreen(videoPath: String) { val context = LocalContext.current // 预加载缩略图,避免重复计算 val thumbnails = remember(videoPath) { generateThumbnails(context, videoPath, 20) } // 获取视频总时长 val totalDuration = remember(videoPath) { val retriever = MediaMetadataRetriever() retriever.setDataSource(videoPath) val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0 retriever.release() duration } // 管理起止位置状态 var startPosition by remember { mutableStateOf(0f) } var endPosition by remember { mutableStateOf(1f) } Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { // 视频预览窗口:展示选中片段的实时预览 VideoPlayer( videoPath = videoPath, startMs = (startPosition * totalDuration).toLong(), endMs = (endPosition * totalDuration).toLong() ) // 裁剪滑块 VideoTrimSlider( thumbnails = thumbnails, totalDuration = totalDuration, startPosition = startPosition, endPosition = endPosition, onStartPositionChange = { startPosition = it }, onEndPositionChange = { endPosition = it } ) // 确认裁剪按钮:调用已完成的核心裁剪逻辑 Button( onClick = { val startMs = (startPosition * totalDuration).toLong() val endMs = (endPosition * totalDuration).toLong() // 执行视频裁剪操作 trimVideo(videoPath, startMs, endMs) }, modifier = Modifier.fillMaxWidth().padding(top = 16.dp) ) { Text("确认裁剪") } } }
三、优化方向
- 用Coil/Glide加载缩略图,配合内存缓存避免OOM
- 给手柄拖拽添加平滑动画,提升交互体验
- 支持轨道缩放,方便精确调整裁剪边界
- 添加实时时间显示,展示选中片段的起止时间和时长
内容的提问来源于stack exchange,提问作者Oluwafemi
相关产品推荐
相关产品推荐

