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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 21:55:01