当前位置: 首页 > news >正文

视频截取中的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 中十分常见。有了上传视频的功能&#xff0c;就不可避免地需要提供截取和编辑的选项。如果我们过度依赖第三方库&#xff0c;项目的代码可能会变得异常臃肿&#xff0c;因为这些库往往包含许多我们用不到的功能&#xff0c;而且它们的 UI 样式和功能…...

java设计模式--结构型模式

结构性模式&#xff1a;适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式 适配器模式 适配器模式&#xff08;Adapter Pattern&#xff09; 充当两个不兼容接口之间的桥梁&#xff0c;属于结构型设计模式。目的是将一个类的接口转换为另一个接口&am…...

消息中间件:Kafka消息丢失与堆积问题分析与解决方案

消息中间件&#xff1a;Kafka消息丢失与堆积问题分析与解决方案 Kafka作为分布式消息系统&#xff0c;广泛应用于实时数据流处理、大数据分析等领域。然而&#xff0c;在实际应用中&#xff0c;Kafka可能会面临消息丢失和消息堆积的问题&#xff0c;这些问题如果得不到有效处理…...

mac终端代理配置指南

终端代理配置指南 在 macOS 中&#xff0c;你可以通过几种不同的方法来配置终端代理。这里介绍两种常见的设置方式&#xff1a;使用 alias 和 shell 函数。 方法 1&#xff1a;使用 Alias 配置代理 打开终端配置文件 默认情况下&#xff0c;macOS 终端使用的是 zsh。如果你的系…...

mbedTLS生成客户端,服务端密钥及CA证书

1. mbedTLS源码&#xff1a;https://github.com/Mbed-TLS/mbedtls.git 2. 生成步骤&#xff1a; 2.1 编译上述源码 2.2 生成CA私钥和自签名证书&#xff1a; 进入编译的build目录&#xff0c;比如&#xff1a;/mbedtls-development/build# 2.2.1生成CA私钥 执行下面的命令&…...

如何有效应对突发技术故障:以网易云音乐为例

引言 在互联网行业&#xff0c;任何一个在线服务都可能遭遇突发的技术故障。这些故障不仅影响用户体验&#xff0c;还可能对公司的品牌形象造成损害。因此&#xff0c;如何快速响应并高效解决这些问题成为了每一个开发团队的重要课题。本文将以网易云音乐在2024年8月19日下午遭…...

上传文件到github仓库

REF: https://blog.csdn.net/litianxiang_kaola/article/details/74075151 已有repository&#xff0c;往仓库里更新内容 点击gitlab里的clone 在git bash中使用git clone&#xff0c;这个时候会将网上的仓库下载到本地&#xff0c;你可以把想要更新的内容直接拖到仓库里 …...

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 &#xff0c;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 协议分析

注&#xff1a; 1. 本文所分析的fastboot源码不是android下的源码&#xff0c;而是恩智浦芯片厂商在IMX6UL芯片的uboot源码中自己实现的源码&#xff0c;二者不同&#xff0c;请读者注意区分。一些图片是网上找到的&#xff0c;出处不好注明&#xff0c;请见谅。 2. 分析fastbo…...

redis hash类型的命令

1.hset 格式&#xff1a; hset key field value [field value ...]&#xff08;value是字符串&#xff09; 返回值&#xff1a;设置成功的键值对的个数 2.hget&#xff1a;获取键值对 格式:hget key field 3.hexists&#xff1a;判断hash中是否存在指定 格式&#xff1a;…...

【OpenCV】 中使用 Lucas-Kanade 光流进行对象跟踪和路径映射

文章目录 一、说明二、什么是Lucas-Kanade 方法三、Lucas-Kanade 原理四、代码实现4.1 第 1 步&#xff1a;用户在第一帧绘制一个矩形4.2 第 2 步&#xff1a;从图像中提取关键点4.3 第 3 步&#xff1a;跟踪每一帧的关键点 一、说明 本文针对基于光流法的目标追踪进行叙述&am…...

ES 支持乐观锁吗?如何实现的?

本篇主要介绍一下Elasticsearch的并发控制和乐观锁的实现原理&#xff0c;列举常见的电商场景&#xff0c;关系型数据库的并发控制、ES的并发控制实践。 并发场景 不论是关系型数据库的应用&#xff0c;还是使用Elasticsearch做搜索加速的场景&#xff0c;只要有数据更新&…...

前端宝典十一:前端工程化稳定性方案

一、工程化体系介绍 1、什么是前端工程化 前端工程化 前端 软件工程&#xff1b;前端工程化 将工程方法系统化地应用到前端开发中&#xff1b;前端工程化 系统、严谨、可量化的方法开发、运营和维护前端应用程序&#xff1b;前端工程化 基于业务诉求&#xff0c;梳理出最…...

yum 数据源的切换

本来准备安装一个ntp 服务器时间进行同步&#xff0c;但是使用yum install ntp -y 但是却失败了 原因是yum自带的镜像源不能用了&#xff0c;所以要想使用yum 多功能只能切换yum 对应的镜像源了 如果你的服务商是可以使用wget命令的&#xff1a; wget -O /etc/yum.repos.d/Ce…...

MySQL入门学习-命令行工具.mysqlbinlog

MySQL 命令行工具mysqlbinlog用于处理二进制日志文件。 一、关于mysqlbinlog工具的详细介绍&#xff1a; 1、命令行工具mysqlbinlog的特点和使用方法&#xff1a; - 特点&#xff1a; - 可以解析和查看二进制日志文件的内容。 - 支持多种输出格式&#xff0c;如文本、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(_("该区名已存在&#xff0c;无需再填加…...

使用预训练的 ONNX 格式的目标检测模型(基于 YOLOv8n-pose)姿态监测

具体步骤如下&#xff1a; 加载图像&#xff1a; 从指定路径读取一张图像&#xff08;这里假设图像名为bus.jpg&#xff09;。将图像从 BGR 颜色空间转换为 RGB 颜色空间。 图像预处理&#xff1a; 计算图像的高度、宽度&#xff0c;并确定其中的最大值作为新图像的边长。创建一…...

matlab实现模拟退火算法

模拟退火算法&#xff08;Simulated Annealing, SA&#xff09;是一种通用概率优化算法&#xff0c;用于在给定的大搜索空间内寻找问题的近似全局最优解。该算法灵感来源于物理学中固体物质的退火过程&#xff0c;其中温度逐渐降低&#xff0c;粒子逐渐趋于能量最低状态。 在M…...

【Prettier】代码格式化工具Prettier的使用和配置介绍

前言 前段时间&#xff0c;因为项目的prettier的配置和eslint格式检查有些冲突&#xff0c;在其prettier官网和百度了一些配置相关的资料&#xff0c;在此做一些总结&#xff0c;以备不时之需。 Prettier官网 Prettier Prettier 是一种前端代码格式化工具&#xff0c;支持ja…...

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...

C++:std::is_convertible

C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

centos 7 部署awstats 网站访问检测

一、基础环境准备&#xff08;两种安装方式都要做&#xff09; bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats&#xff0…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)

CSI-2 协议详细解析 (一&#xff09; 1. CSI-2层定义&#xff08;CSI-2 Layer Definitions&#xff09; 分层结构 &#xff1a;CSI-2协议分为6层&#xff1a; 物理层&#xff08;PHY Layer&#xff09; &#xff1a; 定义电气特性、时钟机制和传输介质&#xff08;导线&#…...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版​分享

平时用 iPhone 的时候&#xff0c;难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵&#xff0c;或者买了二手 iPhone 却被原来的 iCloud 账号锁住&#xff0c;这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

Typeerror: cannot read properties of undefined (reading ‘XXX‘)

最近需要在离线机器上运行软件&#xff0c;所以得把软件用docker打包起来&#xff0c;大部分功能都没问题&#xff0c;出了一个奇怪的事情。同样的代码&#xff0c;在本机上用vscode可以运行起来&#xff0c;但是打包之后在docker里出现了问题。使用的是dialog组件&#xff0c;…...

day36-多路IO复用

一、基本概念 &#xff08;服务器多客户端模型&#xff09; 定义&#xff1a;单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力 作用&#xff1a;应用程序通常需要处理来自多条事件流中的事件&#xff0c;比如我现在用的电脑&#xff0c;需要同时处理键盘鼠标…...

Monorepo架构: Nx Cloud 扩展能力与缓存加速

借助 Nx Cloud 实现项目协同与加速构建 1 &#xff09; 缓存工作原理分析 在了解了本地缓存和远程缓存之后&#xff0c;我们来探究缓存是如何工作的。以计算文件的哈希串为例&#xff0c;若后续运行任务时文件哈希串未变&#xff0c;系统会直接使用对应的输出和制品文件。 2 …...

鸿蒙HarmonyOS 5军旗小游戏实现指南

1. 项目概述 本军旗小游戏基于鸿蒙HarmonyOS 5开发&#xff0c;采用DevEco Studio实现&#xff0c;包含完整的游戏逻辑和UI界面。 2. 项目结构 /src/main/java/com/example/militarychess/├── MainAbilitySlice.java // 主界面├── GameView.java // 游戏核…...