曂沅仴駦 发表于 2024-8-27 00:45:37

视频截取中的UI小组件

https://i-blog.csdnimg.cn/direct/3cf98b45b7d74ee2a8f26e60afe07b3b.gif
引言

视频截取在社交类 APP 中十分常见。有了上传视频的功能,就不可避免地必要提供截取和编辑的选项。假如我们过度依靠第三方库,项目的代码可能会变得异常痴肥,因为这些库每每包含很多我们用不到的功能,而且它们的 UI 样式和功能通常比力固定,不支持定制。因此,有条件的话,尽可能自行实现这些功能。
原本我计划直接先容视频截取的实现方式,但发现相关的 UI 计划也非常风趣。假如不把 UI 和视频截取功能联合起来,即使掌握了截取技术,也可能难以打造出一个好用的视频编辑工具。因此,在本篇博客中,我们先来先容视频截取中最常见的 UI 样式和小组件。
组件结构

https://i-blog.csdnimg.cn/direct/da9a8d1bcf454333887d05d2b73d9508.jpeg
我们创建一个继承自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 = { x in
         ...
      }
      // 左侧拖拽回调
      sliderView.leftDragBlock = { 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 = { 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 = { 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小组件,先容了怎样实现镂空效果,以及拖拽事件,尤其是拖拽时临界值的处置处罚。
获取到了裁剪地区之后,我们就可以根据视频的长度来进行视频截取了,那么下一篇博客我们将开始进入视频截取的数据处置处罚部分。




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 视频截取中的UI小组件