视频截取中的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 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小组件,介绍了如何实现镂空效果,以及拖拽事件,尤其是拖拽时临界值的处理。
获取到了裁剪区域之后,我们就可以根据视频的长度来进行视频截取了,那么下一篇博客我们将开始进入视频截取的数据处理部分。
相关文章:
视频截取中的UI小组件
引言 视频截取在社交类 APP 中十分常见。有了上传视频的功能,就不可避免地需要提供截取和编辑的选项。如果我们过度依赖第三方库,项目的代码可能会变得异常臃肿,因为这些库往往包含许多我们用不到的功能,而且它们的 UI 样式和功能…...
java设计模式--结构型模式
结构性模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式 适配器模式 适配器模式(Adapter Pattern) 充当两个不兼容接口之间的桥梁,属于结构型设计模式。目的是将一个类的接口转换为另一个接口&am…...
消息中间件:Kafka消息丢失与堆积问题分析与解决方案
消息中间件:Kafka消息丢失与堆积问题分析与解决方案 Kafka作为分布式消息系统,广泛应用于实时数据流处理、大数据分析等领域。然而,在实际应用中,Kafka可能会面临消息丢失和消息堆积的问题,这些问题如果得不到有效处理…...
mac终端代理配置指南
终端代理配置指南 在 macOS 中,你可以通过几种不同的方法来配置终端代理。这里介绍两种常见的设置方式:使用 alias 和 shell 函数。 方法 1:使用 Alias 配置代理 打开终端配置文件 默认情况下,macOS 终端使用的是 zsh。如果你的系…...
mbedTLS生成客户端,服务端密钥及CA证书
1. mbedTLS源码:https://github.com/Mbed-TLS/mbedtls.git 2. 生成步骤: 2.1 编译上述源码 2.2 生成CA私钥和自签名证书: 进入编译的build目录,比如:/mbedtls-development/build# 2.2.1生成CA私钥 执行下面的命令&…...
如何有效应对突发技术故障:以网易云音乐为例
引言 在互联网行业,任何一个在线服务都可能遭遇突发的技术故障。这些故障不仅影响用户体验,还可能对公司的品牌形象造成损害。因此,如何快速响应并高效解决这些问题成为了每一个开发团队的重要课题。本文将以网易云音乐在2024年8月19日下午遭…...
上传文件到github仓库
REF: https://blog.csdn.net/litianxiang_kaola/article/details/74075151 已有repository,往仓库里更新内容 点击gitlab里的clone 在git bash中使用git clone,这个时候会将网上的仓库下载到本地,你可以把想要更新的内容直接拖到仓库里 …...
clip-path实现图片边角的裁剪
img {clip-path: polygon(0 7px,7px 0,calc(100% - 20px) 0,100% 20px,100% 100%,16px 100%,0 calc(100% - 16px));}每一个逗号隔开的就是路径坐标 左上角的两个点 0 7px ,7px 0 右上角 calc(100% - 20px) 0,100% 20px 相当于通过这些点练成的线的圈起来的部分就是剩…...
【C++ Primer Plus习题】2.7
问题: 解答: #include <iostream> using namespace std;void print(int hour, int minute) {cout << "Time:" << hour << ":" << minute << endl; }int main() {int hour0;int minute 0;cout << "请输入…...
uboot中 fastboot udp 协议分析
注: 1. 本文所分析的fastboot源码不是android下的源码,而是恩智浦芯片厂商在IMX6UL芯片的uboot源码中自己实现的源码,二者不同,请读者注意区分。一些图片是网上找到的,出处不好注明,请见谅。 2. 分析fastbo…...
redis hash类型的命令
1.hset 格式: hset key field value [field value ...](value是字符串) 返回值:设置成功的键值对的个数 2.hget:获取键值对 格式:hget key field 3.hexists:判断hash中是否存在指定 格式:…...
【OpenCV】 中使用 Lucas-Kanade 光流进行对象跟踪和路径映射
文章目录 一、说明二、什么是Lucas-Kanade 方法三、Lucas-Kanade 原理四、代码实现4.1 第 1 步:用户在第一帧绘制一个矩形4.2 第 2 步:从图像中提取关键点4.3 第 3 步:跟踪每一帧的关键点 一、说明 本文针对基于光流法的目标追踪进行叙述&am…...
ES 支持乐观锁吗?如何实现的?
本篇主要介绍一下Elasticsearch的并发控制和乐观锁的实现原理,列举常见的电商场景,关系型数据库的并发控制、ES的并发控制实践。 并发场景 不论是关系型数据库的应用,还是使用Elasticsearch做搜索加速的场景,只要有数据更新&…...
前端宝典十一:前端工程化稳定性方案
一、工程化体系介绍 1、什么是前端工程化 前端工程化 前端 软件工程;前端工程化 将工程方法系统化地应用到前端开发中;前端工程化 系统、严谨、可量化的方法开发、运营和维护前端应用程序;前端工程化 基于业务诉求,梳理出最…...
yum 数据源的切换
本来准备安装一个ntp 服务器时间进行同步,但是使用yum install ntp -y 但是却失败了 原因是yum自带的镜像源不能用了,所以要想使用yum 多功能只能切换yum 对应的镜像源了 如果你的服务商是可以使用wget命令的: wget -O /etc/yum.repos.d/Ce…...
MySQL入门学习-命令行工具.mysqlbinlog
MySQL 命令行工具mysqlbinlog用于处理二进制日志文件。 一、关于mysqlbinlog工具的详细介绍: 1、命令行工具mysqlbinlog的特点和使用方法: - 特点: - 可以解析和查看二进制日志文件的内容。 - 支持多种输出格式,如文本、SQ…...
WARNING XXX is not overriding the create method in batch
WARNING XXX is not overriding the create method in batch api.modeldef create(self, vals):quvals[name]youqu self.env[crm.qu].sudo().search([(name, , qu),(shi_id,,vals[shi_id])])if len(youqu)>0:raise UserError(_("该区名已存在,无需再填加…...
使用预训练的 ONNX 格式的目标检测模型(基于 YOLOv8n-pose)姿态监测
具体步骤如下: 加载图像: 从指定路径读取一张图像(这里假设图像名为bus.jpg)。将图像从 BGR 颜色空间转换为 RGB 颜色空间。 图像预处理: 计算图像的高度、宽度,并确定其中的最大值作为新图像的边长。创建一…...
matlab实现模拟退火算法
模拟退火算法(Simulated Annealing, SA)是一种通用概率优化算法,用于在给定的大搜索空间内寻找问题的近似全局最优解。该算法灵感来源于物理学中固体物质的退火过程,其中温度逐渐降低,粒子逐渐趋于能量最低状态。 在M…...
【Prettier】代码格式化工具Prettier的使用和配置介绍
前言 前段时间,因为项目的prettier的配置和eslint格式检查有些冲突,在其prettier官网和百度了一些配置相关的资料,在此做一些总结,以备不时之需。 Prettier官网 Prettier Prettier 是一种前端代码格式化工具,支持ja…...
【AI搜索时代生存指南】:Perplexity vs Google搜索的5大核心差异,90%的开发者还不知道的关键决策点
更多请点击: https://intelliparadigm.com 第一章:AI搜索时代的技术范式迁移 传统关键词匹配式搜索正被语义理解、上下文感知与生成式推理深度重构。AI搜索不再仅返回文档链接,而是直接合成答案、推演逻辑链、调用工具并动态验证结果——这标…...
League-Toolkit终极指南:英雄联盟玩家的智能自动化神器
League-Toolkit终极指南:英雄联盟玩家的智能自动化神器 【免费下载链接】League-Toolkit An all-in-one toolkit for LeagueClient. Gathering power 🚀. 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit 如果你是英雄联盟玩家&…...
League Akari:3步打造你的英雄联盟智能游戏助手,告别繁琐操作
League Akari:3步打造你的英雄联盟智能游戏助手,告别繁琐操作 【免费下载链接】League-Toolkit An all-in-one toolkit for LeagueClient. Gathering power 🚀. 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit League A…...
LinkedOM与JSDOM性能对比:10倍速度提升的秘诀
LinkedOM与JSDOM性能对比:10倍速度提升的秘诀 【免费下载链接】linkedom A triple-linked lists based DOM implementation. 项目地址: https://gitcode.com/gh_mirrors/li/linkedom 在现代Web开发中,DOM解析和操作性能直接影响应用响应速度。Lin…...
Origin9.1绘图避坑指南:从数据导入到论文级.tif图保存的完整流程
Origin9.1科研绘图全流程避坑指南:从数据导入到论文级.tif输出 科研绘图是论文写作中不可或缺的一环,而Origin9.1作为经典的数据可视化工具,在学术界有着广泛的应用。然而,从原始数据到最终符合期刊要求的图表,这一过程…...
Chromatic深度解析:基于QuickJS的跨平台动态代码注入框架实现原理
Chromatic深度解析:基于QuickJS的跨平台动态代码注入框架实现原理 【免费下载链接】chromatic Universal modifier for Chromium/V8 | 广谱注入 Chromium/V8 的通用修改器 项目地址: https://gitcode.com/gh_mirrors/be/chromatic 你是否曾经遇到过这样的技术…...
从零到一:在云端服务器安全部署Jupyter Lab/Notebook
1. 云端服务器基础环境准备 刚拿到一台全新的云服务器时,就像搬进毛坯房需要先通水电。我以阿里云ECS为例(其他云服务商操作类似),从系统初始化到基础安全加固,带你走完这段必经之路。 首先用SSH连接服务器时ÿ…...
算法基础(十一)—— 递归树如何看懂分治算法的运行时间
1. 定位导航 前面已经学习了分治思想: 分解 → 解决 → 合并分治算法经常可以写成递归式。 例如归并排序: 先把数组拆成左右两半; 分别排序左右两半; 再合并两个有序数组。它的运行时间可以粗略写成: T(n)2T(n/2)n T(n…...
5分钟掌握全能文档下载神器:告别付费壁垒,解放你的知识获取能力
5分钟掌握全能文档下载神器:告别付费壁垒,解放你的知识获取能力 【免费下载链接】kill-doc 看到经常有小伙伴们需要下载一些免费文档,但是相关网站浏览体验不好各种广告,各种登录验证,需要很多步骤才能下载文档&#x…...
如何轻松构建多平台直播录制系统的完整指南
如何轻松构建多平台直播录制系统的完整指南 【免费下载链接】stream-rec Automatic streaming record tool. Live stream and bullet comments recorder. 虎牙/抖音/斗鱼/Twitch/PandaTV/微博直播,弹幕自动录制 项目地址: https://gitcode.com/gh_mirrors/st/stre…...
