SwiftUI实现视频修剪器:如何裁剪HStack边缘缩略图适配修剪操作
在SwiftUI中实现可裁剪的视频修剪缩略图
要实现拖动修剪时两端缩略图的比例裁剪效果,核心思路是根据当前修剪时间范围,计算每个缩略图的可见比例,再通过SwiftUI的布局和裁剪工具控制显示区域。以下是具体实现方案:
核心实现代码
import SwiftUI import AVFoundation struct ThumbnailsView: View { let thumbImages: [ThumbImage] @Binding var trimRange: ClosedRange<CMTime> let totalDuration: CMTime let thumbnailWidth: CGFloat = 80 // 单个缩略图的原始宽度 let thumbnailInterval: CMTime = CMTimeMakeWithSeconds(1, preferredTimescale: 600) // 每个缩略图覆盖的视频时长 // 计算单个缩略图对应的视频时间区间 private func timeRange(for thumb: ThumbImage) -> ClosedRange<CMTime> { let start = thumb.time - thumbnailInterval / 2 let end = thumb.time + thumbnailInterval / 2 return max(start, .zero)...min(end, totalDuration) } var body: some View { HStack(alignment: .center, spacing: 0) { ForEach(thumbImages, id: \.time) { thumbImage in let thumbTimeRange = timeRange(for: thumbImage) // 计算缩略图与修剪范围的重叠区间 let overlapStart = max(thumbTimeRange.lowerBound, trimRange.lowerBound) let overlapEnd = min(thumbTimeRange.upperBound, trimRange.upperBound) if overlapStart >= overlapEnd { // 完全不在修剪范围内,隐藏 Color.clear.frame(width: 0) } else { // 计算可见区域比例 let thumbTotalSeconds = CMTimeGetSeconds(thumbTimeRange.upperBound - thumbTimeRange.lowerBound) let overlapSeconds = CMTimeGetSeconds(overlapEnd - overlapStart) let visibleRatio = overlapSeconds / thumbTotalSeconds Image(uiImage: thumbImage.image) .resizable() .scaledToFit() .frame(width: thumbnailWidth * visibleRatio) // 调整对齐,确保显示裁剪后的正确区域 .alignmentGuide(.leading) { _ in thumbTimeRange.lowerBound < trimRange.lowerBound ? thumbnailWidth * (1 - visibleRatio) : 0 } .clipped() .border(Color.white, width: 5) } } } .padding(5) } } struct ThumbImage { var time: CMTime let image: UIImage } // 配套的修剪控制视图示例 struct VideoTrimmerView: View { @State private var trimRange: ClosedRange<CMTime> = .zero...CMTimeMakeWithSeconds(10, preferredTimescale: 600) let totalDuration: CMTime = CMTimeMakeWithSeconds(10, preferredTimescale: 600) @State private var thumbImages: [ThumbImage] = [] var body: some View { VStack(spacing: 20) { ThumbnailsView( thumbImages: thumbImages, trimRange: $trimRange, totalDuration: totalDuration ) // 左右拖动修剪控件 HStack { Circle() .frame(width: 24, height: 24) .foregroundColor(.blue) .gesture( DragGesture().onChanged { value in guard let parentWidth = value.view?.superview?.bounds.width else { return } let dragRatio = value.translation.width / parentWidth let newStartSeconds = CMTimeGetSeconds(trimRange.lowerBound) + dragRatio * CMTimeGetSeconds(totalDuration) let newStart = CMTimeMakeWithSeconds(max(newStartSeconds, 0), preferredTimescale: 600) trimRange = newStart...trimRange.upperBound } ) Spacer() Circle() .frame(width: 24, height: 24) .foregroundColor(.blue) .gesture( DragGesture().onChanged { value in guard let parentWidth = value.view?.superview?.bounds.width else { return } let dragRatio = value.translation.width / parentWidth let newEndSeconds = CMTimeGetSeconds(trimRange.upperBound) + dragRatio * CMTimeGetSeconds(totalDuration) let newEnd = CMTimeMakeWithSeconds(min(newEndSeconds, CMTimeGetSeconds(totalDuration)), preferredTimescale: 600) trimRange = trimRange.lowerBound...newEnd } ) } .padding(.horizontal) } .onAppear { // 这里模拟加载缩略图,实际项目中替换为真实的视频帧生成逻辑 for i in 0..<10 { let time = CMTimeMakeWithSeconds(Double(i), preferredTimescale: 600) thumbImages.append(ThumbImage(time: time, image: UIImage(systemName: "film")!)) } } } }
关键逻辑说明
- 时间区间计算:
timeRange(for:)函数根据缩略图的时间点,计算该缩略图对应的视频时间范围,需要和你生成缩略图的逻辑保持一致(比如你是每0.5秒生成一帧,就把thumbnailInterval改成0.5秒)。 - 重叠区域计算:通过对比缩略图的时间范围和当前修剪范围,得到实际可见的时间区间,进而算出可见比例。
- 布局调整:利用
alignmentGuide调整被裁剪缩略图的位置,确保显示的是修剪范围内的部分(比如左侧被裁剪的缩略图,会向左偏移隐藏左侧区域,只显示右侧可见部分)。 - 裁剪控制:
clipped()修饰符会自动裁剪超出frame的图像内容,配合按比例设置的frame,实现部分显示效果。
注意事项
- 缩略图生成逻辑要和时间区间计算匹配,否则裁剪位置会出现偏差。
- 拖动手势的比例计算建议使用
GeometryReader获取ThumbnailsView的实际宽度,替代示例中的屏幕宽度,提升精度。 - 如果缩略图数量过多,可将
HStack包裹在ScrollView中,同时处理拖动时的滚动联动逻辑。
内容的提问来源于stack exchange,提问作者Deepak Sharma
相关产品推荐
相关产品推荐

