视频截取中的UI小组件

引言

视频截取在社交类 APP 中十分常见。有了上传视频的功能,就不可避免地需要提供截取和编辑的选项。如果我们过度依赖第三方库,项目的代码可能会变得异常臃肿,因为这些库往往包含许多我们用不到的功能,而且它们的 UI 样式和功能通常比较固定,不支持定制。因此,有条件的话,尽可能自行实现这些功能。

原本我打算直接介绍视频截取的实现方式,但发现相关的 UI 设计也非常有趣。如果不把 UI 和视频截取功能结合起来,即使掌握了截取技术,也可能难以打造出一个好用的视频编辑工具。因此,在本篇博客中,我们先来介绍视频截取中最常见的 UI 样式和小组件。

组件结构

我们创建一个继承自UIView的SVVideoEditBar类,整个编辑的操作小组件可以分为播放控制和截取控制两部分:

播放控制

第一部分是播放和暂停按钮,控制截取后视频的播放和暂停功能,这里比较简单只需要一个按钮就可以实现。

    /// 播放按钮
    private var playButton = UIButton()
    /// 添加播放按钮
    private func addPlayerButton() {
        playButton.setImage(UIImage(named: "icon_play_light_fill_24"), for: .normal)
        playButton.setImage(UIImage(named: "icon_pause_light_fill_24"), for: .selected)
        self.addSubview(playButton)
        playButton.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(16.0)
            make.centerY.equalToSuperview()
            make.width.height.equalTo(24.0)
        }
    }

截取控制

第二部分为截取控制部分可以再详细划分为展示部分和操作部分。

展示

对于展示部分我们采用UICollectionView来显示视频获取到的缩略图。

    /// 列表
    private var collectionView:UICollectionView!
    /// 添加列表
    private func addCollectionView() {
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = 0.0
        layout.scrollDirection = .horizontal
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.contentInset = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0)
        collectionView.backgroundColor = UIColor.clear
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        self.addSubview(collectionView)
        collectionView.snp.makeConstraints { make in
            make.leading.equalTo(lineView.snp.trailing).offset(20.0)
            make.trailing.equalToSuperview().offset(-20.0)
            make.top.equalToSuperview().offset(8.0)
            make.bottom.equalToSuperview().offset(-8.0)
        }
        collectionView.backgroundColor = .red
    }

关于列表中图片的尺寸会根据视频的尺寸来确定,所以我们将大小在代理方法中进行设置

extension SVVideoEditBar: UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        
        return CGSize(width: 40.0, height: collectionView.bounds.height)
    }
}
滑动遮罩

遮罩也分为两部分,一部分为黄色的边框,边框内的内容表示的是视频截取后保留的部分,而黄色边框以外的黑色半透明的遮罩则表示视频截取后舍弃的部分。

黄色的部分由我们自定义的视图SVEditorSliderView来实现。

    /// 可拖拽滑动视图
    private var sliderView = SVEditorSliderView()
    /// 添加滑动视图
    private func addSliderView() {
        self.addSubview(sliderView)
        sliderView.layer.cornerRadius = 8.0
        sliderView.backgroundColor = UIColor.yellow
        updateLeftRightOffset()
        // 右侧拖拽回调
        sliderView.rightDragBlock = { [weak self] x in
           ...
        }
        // 左侧拖拽回调
        sliderView.leftDragBlock = { [weak self] x in
          ....
        }
    }
    /// 更新左右侧偏移
    private func updateLeftRightOffset() {
        sliderView.snp.remakeConstraints { make in
            make.leading.equalTo(lineView.snp.trailing).offset(leftOffset)
            make.top.bottom.equalToSuperview()
            make.trailing.equalToSuperview().offset(rightOffset)
        }
    }

两侧的黑色半透明遮罩直接通过UIView设置背景颜色的方式来实现

    /// 左侧蒙层
    private var leftMaskView = UIView()
    /// 右侧蒙层
    private var rightMaskView = UIView()
    /// 添加左侧蒙层
    private func addLeftMaskView() {
        leftMaskView.backgroundColor = UIColor.black.withAlphaComponent(0.3)
        self.addSubview(leftMaskView)
        leftMaskView.snp.makeConstraints { make in
            make.top.bottom.equalToSuperview()
            make.leading.equalTo(self.lineView.snp.trailing)
            make.trailing.equalTo(sliderView.snp.leading)
        }
    }
    
    /// 添加右侧蒙层
    private func addRightMaskView() {
        rightMaskView.backgroundColor = UIColor.black.withAlphaComponent(0.3)
        self.addSubview(rightMaskView)
        rightMaskView.snp.makeConstraints { make in
            make.top.bottom.equalToSuperview()
            make.leading.equalTo(sliderView.snp.trailing)
            make.trailing.equalToSuperview()
        }
    }
进度条

另外还有一个在播放时会跟随播放进度而移动的进度条,直接使用UIView加阴影的方式来实现。

class SVProgressLineView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .white
        self.layer.cornerRadius = 1.0
        // 阴影
        self.layer.shadowColor = UIColor.black.cgColor
        self.layer.shadowOffset = CGSize(width: -1, height: 0)
        self.layer.shadowOpacity = 0.5
        self.layer.shadowRadius = 2.0
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}
    /// 进度线
    private var progressLineView = SVProgressLineView()
    /// 添加进度线
    private func addProgressLineView() {
        self.addSubview(progressLineView)
        progressLineView.snp.makeConstraints { make in
            make.leading.equalTo(sliderView).offset(20.0 + 10.0)
            make.centerY.equalToSuperview()
            make.width.equalTo(2.0)
            make.height.equalTo(self.snp.height).inset(8.0)
        }
    }

实现操作视图 - SVEditorSliderView

接下来我们把重点集中到SVEditorSliderView上面,首先它需要一个镂空效果,来显示底部的缩图列表,另外它的两侧还需要可拖拽来修改视频的截取区域。

镂空效果

那我们先来实现它的镂空效果,采用图层的mask属性和贝塞尔曲线结合来实现镂空。

    /// maskLayer
    private let maskLayer = CAShapeLayer()
    /// path
    private var path = UIBezierPath()
    /// fullPath
    private var fullPath = UIBezierPath()
    override init(frame: CGRect) {
        super.init(frame: frame)
        maskLayer.backgroundColor = UIColor.black.cgColor
        self.layer.mask = maskLayer
        maskLayer.fillRule = .evenOdd
        ...
    }
    override func layoutSubviews() {
        let width = self.bounds.width
        let height = self.bounds.height
        fullPath = UIBezierPath(rect: self.bounds)
        path = UIBezierPath(rect: CGRect(x: 20.0, y: 8.0, width: width - 40.0, height: height - 16.0))
        fullPath.append(path)
        fullPath.usesEvenOddFillRule = true
        maskLayer.path = fullPath.cgPath
        ....
    }

拖拽事件

在视图的最左侧和最右侧添加可拖拽的UIView视图,并处理拖拽事件,由于该视图的布局在父视图上,所以我们选择将退拽事件回调出去来处理。

    /// 右侧可拖拽视图
    private let rightDragView = UIView()
    /// 左侧可拖拽视图
    private let leftDragView = UIView()
    /// 右侧退拽回调
    var rightDragBlock:((CGFloat)->Void)?
    /// 左侧拖拽回调
    var leftDragBlock:((CGFloat)->Void)?
    /// 添加右侧可拖拽视图
    private func addRightDragView() {
//        rightDragView.backgroundColor = .white
        self.addSubview(rightDragView)
        let pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:)))
        rightDragView.addGestureRecognizer(pan)
        rightDragView.isUserInteractionEnabled = true
    }
    
    /// 添加左侧可拖拽视图
    private func addLeftDragView() {
//        leftDragView.backgroundColor = .white
        self.addSubview(leftDragView)
        let pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:)))
        leftDragView.addGestureRecognizer(pan)
        leftDragView.isUserInteractionEnabled = true
    }
    
    @objc private func panAction(_ pan:UIPanGestureRecognizer) {
        // 获取视图
        let view = pan.view
        if view == rightDragView {
            let translation = pan.translation(in: self)
            let x = translation.x
            rightDragBlock?(x)
        } else if view == leftDragView {
            let translation = pan.translation(in: self)
            let x = translation.x
            leftDragBlock?(x)
        }
        pan.setTranslation(.zero, in: self)
    }
    override func layoutSubviews() {
        let width = self.bounds.width
        let height = self.bounds.height
        ...
        rightDragView.frame = CGRect(x: width - 20.0, y: 0.0, width: 20.0, height: height)
        leftDragView.frame = CGRect(x: 0.0, y: 0.0, width: 20.0, height: height)
    }

拖拽处理

处理拖拽事件是个细活,在拖拽过程中我们需要更新sliderView的布局约束,我们把它分成两个部分来讨论。

右侧拖拽事件

在SVVideoEditBar类中我们还定义另外两个CGFloat类型属性

    /// 左侧偏移

    private var leftOffset:CGFloat = 0.0

    /// 右侧偏移

    private var rightOffset:CGFloat = 0.0

分别表示sliderView的左侧约束的偏移量和右侧约束的偏移量,默认都为0.0。

在进行右侧退拽时,我们首先需要注意的是,往右拖拽时不能超过SVVideoEditBar的最右端,而往左退拽时不能超过sliderView自己的最左端的拖拽视图。

        // 右侧拖拽回调
        sliderView.rightDragBlock = { [weak self] x in
            guard let self = self else { return }
            // 限制右侧往右的拖拽范围
            if self.sliderView.frame.maxX >= self.bounds.width && x > 0 {
                print("右侧往右拖拽到最大范围")
                self.rightOffset = 0.0
                self.updateLeftRightOffset()
                return
            }
            // 限制右侧往左的拖拽范围
            if self.sliderView.frame.maxX <= (self.sliderView.frame.minX+40.0) && x < 0 {
                print("右侧往左拖拽到最小范围")
                self.rightOffset = -(self.bounds.width - self.sliderView.frame.minX - 40.0)
                self.updateLeftRightOffset()
                return
            }
            self.rightOffset = self.rightOffset + x
            self.updateLeftRightOffset()
        }

左侧拖拽事件

当我们拖拽左侧是视图时,需要注意当往左侧拖拽时不能超过左侧的起始位置,也就是竖线的最最右侧。而往右拖拽时同样也不能超过右侧的拖拽视图。

        // 左侧拖拽回调
        sliderView.leftDragBlock = { [weak self] x in
            guard let self = self else { return }
            // 限制左侧往左的拖拽范围
            if self.sliderView.frame.minX <= self.lineView.frame.maxX && x < 0 {
                print("左侧往左拖拽到最小范围")
                self.leftOffset = 0.0
                self.updateLeftRightOffset()
                return
            }
            // 限制左侧往右的拖拽范围
            if self.sliderView.frame.minX >= (self.sliderView.frame.maxX-40.0) && x > 0 {
                print("左侧往右拖拽到最大范围")
                self.leftOffset = self.sliderView.frame.maxX - 40.0 - self.lineView.frame.maxX
                self.updateLeftRightOffset()
                return
            }
            self.leftOffset = self.leftOffset + x
            self.updateLeftRightOffset()
        }

这样像GIF图片一样的视频剪裁小组件的UI部分就实现了。

结语

本篇博客主要介绍了视频截取中的UI小组件,介绍了如何实现镂空效果,以及拖拽事件,尤其是拖拽时临界值的处理。

获取到了裁剪区域之后,我们就可以根据视频的长度来进行视频截取了,那么下一篇博客我们将开始进入视频截取的数据处理部分。

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值