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

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,实现部分显示效果。

注意事项

  1. 缩略图生成逻辑要和时间区间计算匹配,否则裁剪位置会出现偏差。
  2. 拖动手势的比例计算建议使用GeometryReader获取ThumbnailsView的实际宽度,替代示例中的屏幕宽度,提升精度。
  3. 如果缩略图数量过多,可将HStack包裹在ScrollView中,同时处理拖动时的滚动联动逻辑。

内容的提问来源于stack exchange,提问作者Deepak Sharma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 06:33:18