当前位置: 首页 > 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…...

【计算机网络】网络基础

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在学习c和算法 ✈️专栏&#xff1a;Linux &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章有啥瑕疵&#xff0c;希望大佬指点一二 如果文章对…...

MFC在对话框中实现打印和打印预览

首先在这里感谢互联网的大哥们&#xff01;同时我讨厌动不动就是要vip才能查看&#xff01;所以我写的不需要vip就能看。只求点个赞。 直接上代码&#xff0c;新建6个文件CPrintFrame.cpp&#xff1b;CPrintFrame.h&#xff1b;CPrintPreviewView.cpp&#xff1b;CPrintPrevie…...

移动端页面出现闪屏

v-cloak 的作用和用法 用法&#xff1a; 这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时&#xff0c;这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。官方API {{msg}} HTML 绑定 Vue实例&#xff0c;在页面加载时…...

elasticsearch的高亮查询三种模式查询及可能存在的问题

目录 高亮查询使用介绍 高亮参数 三种分析器 可能存在的查询问题 fvh查询时出现StringIndexOutOfBoundsException越界 检索高亮不正确 参考文档 高亮查询使用介绍 Elasticsearch 的高亮&#xff08;highlight&#xff09;可以从搜索结果中的一个或多个字段中获取突出显…...

【精品实战项目】深度学习预测、深度强化学习优化、附源码数据手把手教学

目录 前言 一、预测算法数据与代码介绍(torch和mxnet都有) 1.1 数据介绍 1.2 代码介绍 1.3 优化介绍 二、深度强化学习算法优化 2.1 DDPG 介绍 DPG--deterministic policy gradient DQN--deep Q-network DDPG--deep deterministic policy gradient 三、其他算法 总结…...

JavaScript 手写仿深拷贝

实现对象参数的深拷贝并返回拷贝之后的新对象&#xff0c;因为参数对象和参数对象的每个数据项的数据类型范围仅在数组、普通对象&#xff08;{}&#xff09;、基本数据类型中且无需考虑循环引用问题&#xff0c;所以不需要做过多的数据类型判断&#xff0c;核心步骤有&#xf…...

spring低版本设置cookie的samesite属性

场景&#xff1a;比较古老的项目了&#xff0c;ssh架子&#xff0c;Chrome 51 开始&#xff0c;浏览器的 Cookie 新增加了一个SameSite属性&#xff0c;可用于防止 CSRF 攻击和用户追踪。因此需要给其字段赋值。 网上找了很多资源&#xff0c;由于jar版本比较低&#xff0c;没有…...

GPT4o编写步进电机控制代码

我给出的要求如下&#xff1a; 基于STM32F407 HAL库&#xff0c;写一个步进电机控制程序&#xff0c;需要控制8个步进电机&#xff0c;我会给出描述步进电机的结构体变量&#xff0c;基于这些变量需要你做出以下功能&#xff0c;电机脉冲通过定时器中断翻转脉冲引脚的电平实现…...

关于Spring Boot的自动配置

目录 1.EnableAutoConfiguration注解 2.SpringBootConfiguration注解 3.Import注解 4.spring.factories 5.总结 &#xff08;1&#xff09;EnableAutoConfiguration &#xff08;2&#xff09;AutoConfigurationImportSelector &#xff08;3&#xff09; SpringFactoriesLoade…...

## 已解决:`java.sql.SQLSyntaxErrorException: SQL语法错误` 异常的正确解决方法,亲测有效!!! ###

1. 问题描述 java.sql.SQLSyntaxErrorException 是 Java 程序在执行 SQL 查询时&#xff0c;因 SQL 语法错误而抛出的异常。通常情况下&#xff0c;错误信息会指示出错的 SQL 语句及错误原因&#xff0c;如拼写错误、关键字遗漏、字段名称错误等。 典型的错误信息如下&#x…...