视频截取中的UI小组件

server/2024/9/29 15:52:09/

引言

视频截取在社交类 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 inmake.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.0layout.scrollDirection = .horizontalcollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)collectionView.contentInset = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0)collectionView.backgroundColor = UIColor.clearcollectionView.showsHorizontalScrollIndicator = falsecollectionView.delegate = selfcollectionView.dataSource = selfcollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")self.addSubview(collectionView)collectionView.snp.makeConstraints { make inmake.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.0sliderView.backgroundColor = UIColor.yellowupdateLeftRightOffset()// 右侧拖拽回调sliderView.rightDragBlock = { [weak self] x in...}// 左侧拖拽回调sliderView.leftDragBlock = { [weak self] x in....}}
    /// 更新左右侧偏移private func updateLeftRightOffset() {sliderView.snp.remakeConstraints { make inmake.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 inmake.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 inmake.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 = .whiteself.layer.cornerRadius = 1.0// 阴影self.layer.shadowColor = UIColor.black.cgColorself.layer.shadowOffset = CGSize(width: -1, height: 0)self.layer.shadowOpacity = 0.5self.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 inmake.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属性和贝塞尔曲线结合来实现镂空。

    /// maskLayerprivate let maskLayer = CAShapeLayer()/// pathprivate var path = UIBezierPath()/// fullPathprivate var fullPath = UIBezierPath()
    override init(frame: CGRect) {super.init(frame: frame)maskLayer.backgroundColor = UIColor.black.cgColorself.layer.mask = maskLayermaskLayer.fillRule = .evenOdd...}
    override func layoutSubviews() {let width = self.bounds.widthlet height = self.bounds.heightfullPath = 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 = truemaskLayer.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 = .whiteself.addSubview(rightDragView)let pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:)))rightDragView.addGestureRecognizer(pan)rightDragView.isUserInteractionEnabled = true}/// 添加左侧可拖拽视图private func addLeftDragView() {
//        leftDragView.backgroundColor = .whiteself.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.viewif view == rightDragView {let translation = pan.translation(in: self)let x = translation.xrightDragBlock?(x)} else if view == leftDragView {let translation = pan.translation(in: self)let x = translation.xleftDragBlock?(x)}pan.setTranslation(.zero, in: self)}
    override func layoutSubviews() {let width = self.bounds.widthlet 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 inguard let self = self else { return }// 限制右侧往右的拖拽范围if self.sliderView.frame.maxX >= self.bounds.width && x > 0 {print("右侧往右拖拽到最大范围")self.rightOffset = 0.0self.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 + xself.updateLeftRightOffset()}

左侧拖拽事件

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

        // 左侧拖拽回调sliderView.leftDragBlock = { [weak self] x inguard let self = self else { return }// 限制左侧往左的拖拽范围if self.sliderView.frame.minX <= self.lineView.frame.maxX && x < 0 {print("左侧往左拖拽到最小范围")self.leftOffset = 0.0self.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.maxXself.updateLeftRightOffset()return}self.leftOffset = self.leftOffset + xself.updateLeftRightOffset()}

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

结语

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

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


http://www.ppmy.cn/server/106106.html

相关文章

利用ce修改器进行漏洞挖掘(内存修改)

CE修改器原理 ce修改器可以修改内存数值&#xff0c;因为有些程序会把一些值放在本地&#xff0c;然后改动的时候访问的本地的值&#xff0c;修改之后&#xff0c;客户端服务器再次发生交互的时候&#xff0c;把修改的值发出去&#xff0c;可能会影响到服务器的数据&#xff0…

【Qt】dialog设置无边框之后,模态窗口失效,无法阻止与其他窗口交互

在Qt中自定义一个dialog 并且在显示这个对话框的时候&#xff0c;想要指定他的位置&#xff1a; 我在这个dialog的构造函数中设置了他为模态窗口 this->setModal(true);但是由于我的业务需求&#xff0c;我还设置了它为无边框窗口 this->setWindowFlags(Qt::WindowSta…

SpringCloud Gateway及 Springboot 服务 跨域配置

SpringCloud Gateway 跨域配置 配置文件 #跨域支持 spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-headers[0]* spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-methods[0]* spring.cloud.gateway.globalcors.cors-configuration…

FPGA上板项目(三)——RAM测试

目录 实验内容实验原理实验步骤实验用时序波形HDL 代码仿真综合实现上板测试 实验内容 对 FPGA 内部的 RAM 进行数据读写操作。 实验原理 RAM &#xff08;Random Access Memory&#xff09;&#xff0c;是可以进行数据交换的存储器&#xff0c;可读可写&#xff1b;而 ROM&…

k8s流程叙述

Kubernetes (K8s) 是一个开源的&#xff0c;用于管理容器化应用的平台&#xff0c;它提供了部署&#xff0c;扩展和管理容器化应用的机制。 以下是一个简单的Kubernetes部署流程的描述&#xff1a; 1、安装和设置Kubernetes集群。 2、创建一个Docker镜像&#xff0c;并把应用…

pdf怎么加水印?5个pdf加水印小妙招,学会就不愁了!(亲测有效)

水印是一种半透明的图案&#xff0c;通常包含徽标或印章&#xff0c;用以标识文档或图像的创作者。除了用于标识&#xff0c;水印还可以有效地保护机密信息。如果您想在pdf文件中添加水印&#xff0c;有两种主要方法&#xff1a;您可以直接将水印嵌入到pdf文件中&#xff0c;或…

vscode自用插件分享

1. Image preview 通过此插件&#xff0c;当鼠标悬浮在图片的链接上时&#xff0c;可以实时预览该图片&#xff0c;除此之外&#xff0c;还可以看到图片的大小和分辨率。 2. Auto Rename Tag 使用该插件&#xff0c;可以在重命名一个 HTML 标签时&#xff0c;自动重命名 HTML 标…

pnpm快速入门

pnpm快速入门 1.使用pnpm启动项目 pnpm是一个优化的包管理器&#xff0c;它通过锁定工作树的方式来减少依赖安装的开销。要在pnpm环境中启动项目&#xff0c;首先你需要确保已经全局安装了pnpm。然后按照以下步骤操作 克隆项目&#xff1a;如果项目还没有下载&#xff0c;使用…