深入解析iOS视频录制(二):自定义UI的实现

深入解析 iOS 视频录制(一):录制管理核心MWRecordingController 类的设计与实现

深入解析iOS视频录制(二):自定义UI的实现​​​​​​​

深入解析 iOS 视频录制(三):完整录制流程的实现与整合​​​​​​​

引言

在上一篇博客中,我们深入解析了 iOS 视频录制功能的核心类 MWRecordingController 的实现,涵盖了如何管理视频会话、设置输入输出、控制摄像头以及录制的启动与停止等重要功能。本篇博客将接着上文,探索自定义 UI 的实现,重点讲解如何通过 MWRecordingPreview 预览视图、MWRcordingControlView 控制视图以及精心设置的自定义按钮来提升视频录制的用户体验。

我们将逐步揭开这些自定义 UI 组件背后的设计与实现,了解如何通过灵活的视图布局与动画效果,让录制过程更加直观与流畅。希望通过本篇博客,能够帮助大家更深入地理解如何在 iOS 中定制出高质量的视频录制界面。

UI实现

在视频录制功能中,UI 设计不仅仅是视觉呈现,更是与用户交互的桥梁。一个直观、流畅且具有良好反馈的界面,能够大大提升用户的使用体验。通过自定义 UI,我们能够精细控制各个交互细节,确保用户能够快速而顺畅地完成录制操作。

本篇博客将从三个核心部分入手,详细讲解自定义 UI 的实现:

  1. 预览视图的实现:如何设计并展示视频录制的预览内容,确保录制时用户能够实时看到画面。
  2. 控制视图的实现:包括录制、暂停、重新录制以及完成按钮等,通过合理布局和状态管理,使得控制操作清晰易懂。
  3. 顶部导航栏视图的实现:导航栏中的返回按钮与切换摄像头按钮如何与视频录制功能进行无缝集成。

接下来,我们将逐一展开讲解这些关键视图的设计与实现,带大家了解如何通过代码构建一个高效且美观的视频录制界面。

MWRecordingPreview:预览视图的实现

MWRecordingPreview 类是整个视频录制界面中关键部分之一,它负责显示来自摄像头的实时视频预览。在本次实现中,我们简化了设计,确保预览层与录制会话紧密配合,并通过简单的配置来实现画面显示。

以下是 MWRecordingPreview 类的实现代码:

import UIKit
import AVFoundation

class MWRecordingPreview: UIView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        previewLayer.videoGravity = .resizeAspectFill
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override class var layerClass: AnyClass {
        return AVCaptureVideoPreviewLayer.self
    }
    
    /// 预览图层
    var previewLayer: AVCaptureVideoPreviewLayer {
        return layer as! AVCaptureVideoPreviewLayer
    }
    
    /// 设置图层会话
    var session:AVCaptureSession? {
        didSet {
            previewLayer.session = session
        }
    }
}
  1. layerClass:通过重写 layerClass 方法,我们指定了该视图的图层类型为 AVCaptureVideoPreviewLayer,这是一个专门用于显示摄像头预览内容的图层类。
  2. previewLayer:该属性返回类型为A VCaptureVideoPreviewLayer 的图层,我们可以通过它来访问与摄像头相关的配置。
  3. session:这个属性用来设置 AVCaptureSession,它是视频录制功能的核心,管理着输入输出设备以及数据流。通过 didSet 方法,当会话对象被设置时,预览层的 session 会自动与之绑定,从而实现实时预览功能。

通过这种简单的实现,我们就能将摄像头的实时图像呈现在自定义视图 MWRecordingPreview 中,并且具备了灵活的图像填充方式,使用 .resizeAspectFill 可以确保预览画面根据视图的大小自适应显示。

MWRecordingControlView:控制视图的实现

MWRecordingControlView 类负责管理视频录制过程汇总的交互空间,包括录制按钮、录制时间显示、重新录制按钮以及完成按钮。通过合理布局与状态管理,确保用户能够清晰地了解录制进度,并灵活地控制录制行为。

以下是 MWRecordingControlView 类的实现代码:

import UIKit

enum MWRecordingControlState {
    /// 正常
    case normal
    /// 录制中
    case recording
    /// 录制完成
    case finish
}

class MWRecordingControlView: UIView {
    
    /// 录制按钮
    private let recordButton = MWRecordingButton()
    /// 录制时间
    private let recordTimeLabel = UILabel()
    /// 重新录制按钮
    private let reRecordButton = MWRecordingItemButton()
    /// 完成按钮
    private let finishButton = MWRecordingItemButton()
    
    /// 录制按钮点击事件回调
    var recordButtonClickBlock: (() -> Void)?
    /// 重新录制点击回调
    var reRecordButtonClickBlock: (() -> Void)?
    /// 完成点击回调
    var finishButtonClickBlock: (() -> Void)?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
        setLayout()
        setEvent()
        setRecordingState(state: .normal)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        // 录制按钮
        addSubview(recordButton)
        // 录制时间
        addSubview(recordTimeLabel)
        recordTimeLabel.textColor = .white
        recordTimeLabel.font = MWFontHelper.font(name: .nunito, size: 14, weight: .bold)
        // 重新录制按钮
        addSubview(reRecordButton)
        reRecordButton.setImage(UIImage(named: "recording_retake"), for: .normal)
        reRecordButton.setTitle(MWLocaleStringHelper.getString("Retake"), for: .normal)
        reRecordButton.setTitleColor(.white, for: .normal)
        reRecordButton.titleLabel?.font = MWFontHelper.font(name: .nunito, size: 14, weight: .bold)
        reRecordButton.titleLabel?.textAlignment = .center
        // 完成按钮
        addSubview(finishButton)
        finishButton.setImage(UIImage(named: "recording_complete"), for: .normal)
        finishButton.setTitle(MWLocaleStringHelper.getString("Complete"), for: .normal)
        finishButton.setTitleColor(.white, for: .normal)
        finishButton.titleLabel?.font = MWFontHelper.font(name: .nunito, size: 14, weight: .bold)
        finishButton.titleLabel?.textAlignment = .center
    }
    
    private func setLayout() {
        // 录制按钮
        recordButton.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.top.equalToSuperview()
            make.width.height.equalTo(72.0)
        }
        // 录制时间
        recordTimeLabel.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.top.equalTo(recordButton.snp.bottom).offset(12.0)
            make.height.equalTo(19.0)
        }
        // 重新录制按钮
        reRecordButton.snp.makeConstraints { make in
            make.trailing.equalTo(recordButton.snp.leading).offset(-44.0)
            make.width.equalTo(64.0)
            make.height.equalTo(63.0)
            make.centerY.equalTo(recordButton)
        }
        // 完成按钮
        finishButton.snp.makeConstraints { make in
            make.leading.equalTo(recordButton.snp.trailing).offset(44.0)
            make.width.equalTo(64.0)
            make.height.equalTo(63.0)
            make.centerY.equalTo(recordButton)
        }
    }
    
    private func setEvent() {
        // 录制按钮
        recordButton.addTarget(self, action: #selector(recordButtonClick), for: .touchUpInside)
        // 重新录制按钮
        reRecordButton.addTarget(self, action: #selector(reRecordButtonClick), for: .touchUpInside)
        // 完成按钮
        finishButton.addTarget(self, action: #selector(finishButtonClick), for: .touchUpInside)
    }
    
    /// 录制事件
    @objc private func recordButtonClick() {
        recordButtonClickBlock?()
    }
    
    /// 重新录制事件
    @objc private func reRecordButtonClick() {
        reRecordButtonClickBlock?()
    }
    
    /// 完成事件
    @objc private func finishButtonClick() {
        finishButtonClickBlock?()
    }
    
    /// 设置录制状态
    /// - Parameter state: 状态
    func setRecordingState(state: MWRecordingControlState) {
        switch state {
        case .normal:
            recordButton.isSelected = true
            recordTimeLabel.isHidden = true
            reRecordButton.isHidden = true
            finishButton.isHidden = true
        case .recording:
            recordButton.isSelected = false
            recordTimeLabel.isHidden = false
            reRecordButton.isHidden = true
            finishButton.isHidden = true
        case .finish:
            recordButton.isSelected = true
            recordTimeLabel.isHidden = false
            reRecordButton.isHidden = false
            finishButton.isHidden = false
        }
    }
    
    /// 更新时间
    /// - Parameter time: 时间
    func updateTime(time: TimeInterval) {
        recordTimeLabel.text = time.formattedMS
    }
}

MWRecordingControlView 提供了以下几个核心功能:

  1. 录制按钮(recordButton):用于控制录制的开始和暂停。根据状态变化,按钮的样式会自动调整。
  2. 录制时间(recordTimeLabel):显示当前录制的时长,动态更新。
  3. 重新录制按钮(reRecordButton):在录制完成后,允许用户重新开始录制。
  4. 完成按钮(finishButton):会在点击完成时,将录制好的音频文件传递到需要的地方。

通过设置不同的录制状态(normal、recording和finish),控制视图中的各个按钮和标签的显示与隐藏,确保用户能够清晰地看到当前操作的状态。

MWRecordingButton:录制按钮

MWRecordingButton 是自定义的录制按钮,通过多层图层效果展现录制状态,并使用CALayer和CAGradientLayer创建了独特的视觉效果。其设计目标是提供一个既美观又符合录制功能需求的按钮,用户可以通过点击它来启动或停止视频录制。

以下是 MWRecordingButton 类的实现代码:

import UIKit

class MWRecordingButton: UIButton {
    
    /// 大圆图层
    private let bigCircleLayer = CALayer()
    /// 小圆图层
    private let smallCircleLayer = CALayer()
    /// 方形图层
    private let squareLayer = CAGradientLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLayer()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupLayer() {
        // 大圆
        bigCircleLayer.backgroundColor = UIColor.white.withAlphaComponent(0.6).cgColor
        bigCircleLayer.cornerRadius = 36.0
        layer.addSublayer(bigCircleLayer)
        
        // 小圆
        smallCircleLayer.backgroundColor = UIColor.white.withAlphaComponent(1.0).cgColor
        smallCircleLayer.cornerRadius = 30
        layer.addSublayer(smallCircleLayer)
        
        // 方形
        squareLayer.colors = [UIColor.wm_hex("FF3498").cgColor, UIColor.wm_hex("FF4545").cgColor]
        squareLayer.cornerRadius = 6.0
        layer.addSublayer(squareLayer)
    }
    
    override var isSelected: Bool {
        didSet {
            squareLayer.isHidden = isSelected
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        let width = bounds.width
        let height = bounds.height
        
        // 大圆
        bigCircleLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
        
        // 小圆
        let smallWidth = 60.0
        smallCircleLayer.frame = CGRect(x: (width - smallWidth) / 2, y: (height - smallWidth) / 2, width: smallWidth, height: smallWidth)
        
        // 方形
        let squareWidth = 25.0
        squareLayer.frame = CGRect(x: (width - squareWidth) / 2, y: (height - squareWidth) / 2, width: squareWidth, height: squareWidth)
    }
}
  1. 大圆图层(bigCircleLayer):这个圆形图层充当按钮的背景,使用 CALayer 创建,设置了半透明白色背景,使其看起来既简洁又具有层次感。
  2. 小圆图层 (smallCircleLayer):这个圆形图层在大圆内部显示,颜色为纯白,模拟了录制按钮中的核心圆形。
  3. 方形图层(squareLayer):这个图层使用 CAGradientLayer 绘制了一个渐变色的矩形,位于小圆的中心。它的显示与按钮的选择状(isSelected)态相关,状态为selected时,方形会被隐藏。

通过这种设计,MWRecordingButton在录制和暂停状态之间切换时,能够清晰地反馈状态,用户一眼就能判断当前按钮的作用,同时通过动态的图层切换,使得界面更加生动。

MWRecordingNavigationView:导航栏

MWRecordingNavigationView 是自定义的视频录制界面顶部导航栏视图,它包含了三个主要组件:返回按钮、切换摄像头按钮和一个可定制的中间文案标签。这个视图旨在为用户提供流畅的操作体验,在录制过程中可以方便地进行控制和反馈。

以下是  MWRecordingNavigationView 类的实现代码:

import UIKit

class MWRecordingNavgationView: UIView {

    /// 返回按钮
    private let backButton = UIButton()
    /// 切换摄像头按钮
    private let switchCameraButton = UIButton()
    
    /// 返回按钮点击事件
    var backButtonClickBlock: (() -> Void)?
    /// 切换摄像头按钮点击事件
    var switchCameraButtonClickBlock: (() -> Void)?
    
    /// 隐藏显示切换按钮
    var isHiddenSwitchCameraButton: Bool = false {
        didSet {
            switchCameraButton.isHidden = isHiddenSwitchCameraButton
        }
    }
   
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
        setLayout()
        setEvent()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        // 返回按钮
        addSubview(backButton)
        backButton.setImage(UIImage(named: "navigation_back_white"), for: .normal)
        
        // 切换摄像头按钮
        addSubview(switchCameraButton)
        switchCameraButton.setImage(UIImage(named: "navigation_switch_camera"), for: .normal)
        
    }
    
    private func setLayout() {
        // 设置返回按钮的布局
        backButton.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(16.0)
            make.centerY.equalToSuperview()
            make.width.height.equalTo(32.0)
        }
        
        // 设置切换摄像头按钮的布局
        switchCameraButton.snp.makeConstraints { make in
            make.trailing.equalToSuperview().offset(-16.0)
            make.centerY.equalToSuperview()
            make.width.height.equalTo(32.0)
        }
    }
    
    private func setEvent() {
        // 返回按钮点击事件
        backButton.addTarget(self, action: #selector(backButtonClick), for: .touchUpInside)
        
        // 切换摄像头按钮点击事件
        switchCameraButton.addTarget(self, action: #selector(switchCameraButtonClick), for: .touchUpInside)
    }
    
    @objc private func backButtonClick() {
        backButtonClickBlock?()
    }
    
    @objc private func switchCameraButtonClick() {
        switchCameraButtonClickBlock?()
    }
}
  1. 返回按钮(backButton):通过UIButton实现,提供一个简单的返回操作,用户可以随时退出当前的录制界面。图标为白色箭头,易于辨识。
  2. 切换摄像头(switchCameraButton):也是一个UIButton,用于切换前后摄像头。在某些应用场景下,用户可能需要切换摄像头,这个按钮就提供了这个功能。

结语

在本文中,我们详细探讨了 iOS 视频录制功能中的自定义 UI 实现,包括预览视图、控制视图和导航栏的设计与实现。这些自定义组件不仅提升了用户体验,还确保了操作的流畅性和可控性。通过自定义 MWRecordingPreview 视图,我们为录制过程提供了实时的视频预览;通过设计 MWRecordingControlView 和 MWRecordingButton,我们实现了清晰直观的录制控制;而 MWRecordingNavigationView 则为用户提供了便捷的导航和摄像头切换功能。

这些 UI 组件的灵活性和可定制性,使得在开发过程中可以根据需求对界面进行调整与扩展。在未来的版本中,您可以继续根据产品需求对这些视图进行优化和改进,带给用户更加丰富和优质的录制体验。

通过对这些自定义 UI 组件的解析,我们希望能够帮助开发者们更好地理解和实现 iOS 视频录制功能,并为其应用增添更多个性化的设计。感谢您的阅读,期待您的探索和创新!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/971934.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux系统】生产者消费者模型:基于环形队列(信号量机制)

理论层面 1、环形队列的特性认识 环形队列采用数组模拟,用模运算来模拟环状特性 环形结构起始状态和结束状态都是⼀样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留⼀个空的位置,作为…

【笔记】LLM|Ubuntu22服务器极简本地部署DeepSeek+API使用方式

2025/02/18说明:2月18日~2月20日是2024年度博客之星投票时间,走过路过可以帮忙点点投票吗?我想要前一百的实体证书,经过我严密的计算只要再拿到60票就稳了。一人可能会有多票,Thanks♪(・ω・)&am…

leetcode-414.第三大的数

leetcode-414.第三大的数 code review! 文章目录 leetcode-414.第三大的数一.题目描述二.代码提交 一.题目描述 二.代码提交 class Solution { public:int thirdMax(vector<int>& nums) {set<int> set_v(nums.begin(), nums.end());auto it set_v.rbegin()…

【设计模式】 代理模式(静态代理、动态代理{JDK动态代理、JDK动态代理与CGLIB动态代理的区别})

代理模式 代理模式是一种结构型设计模式&#xff0c;它提供了一种替代访问的方法&#xff0c;即通过代理对象来间接访问目标对象。代理模式可以在不改变原始类代码的情况下&#xff0c;增加额外的功能&#xff0c;如权限控制、日志记录等。 静态代理 静态代理是指创建的或特…

深度学习之图像回归(二)

前言 这篇文章主要是在图像回归&#xff08;一&#xff09;的基础上对该项目进行的优化。&#xff08;一&#xff09;主要是帮助迅速入门 理清一个深度学习项目的逻辑 这篇文章则主要注重在此基础上对于数据预处理和模型训练进行优化前者会通过涉及PCA主成分分析 特征选择 后…

利用分治策略优化快速排序

1. 基本思想 分治快速排序&#xff08;Quick Sort&#xff09;是一种基于分治法的排序算法&#xff0c;采用递归的方式将一个数组分割成小的子数组&#xff0c;并通过交换元素来使得每个子数组元素按照特定顺序排列&#xff0c;最终将整个数组排序。 快速排序的基本步骤&#…

照片模糊怎么变清晰?图生生AI修图-一键清晰放大

当打开手机相册时&#xff0c;那些泛着噪点的合影、细节模糊的风景照、像素化的证件图片&#xff0c;让珍贵时刻蒙上遗憾的面纱。而专业级图像修复工具的门槛&#xff0c;让多数人只能无奈接受这些"不完美的记忆"。AI技术的发展&#xff0c;让普通用户也能轻松拥有专…

Linux 网络与常用操作(适合开发/运维/网络工程师)

目录 OSI 七层协议简介 应用层 传输层 Linux 命令&#xff01;&#xff01;&#xff01; 1. ifconfig 命令 简介 1. 查看网络地址信息 2. 指定开启、或者关闭网卡 3. 修改、设置 IP 地址 4. 修改机器的 MAC 地址信息 5. 永久修改网络设备信息 2. route 路由命令 …

PID控制学习

前言 本篇文章属于PID控制算法的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 PID入门教程-电机控制 倒立摆 持续更新中_哔哩哔哩_bilibili 一…

第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式

第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式 解决按键扫描&#xff0c;松手检测时阻塞的问题实现LED闪烁的非阻塞总结补充&#xff08;为什么不会阻塞&#xff09; 参考江协科技 KEY1和KEY2两者独立控制互不影响 阻塞&#xff1a;如果按下按键不松手&#xff0c;程序就…

【Arxiv 大模型最新进展】PEAR: 零额外推理开销,提升RAG性能!(★AI最前线★)

【Arxiv 大模型最新进展】PEAR: 零额外推理开销&#xff0c;提升RAG性能&#xff01;&#xff08;★AI最前线★&#xff09; &#x1f31f; 嗨&#xff0c;你好&#xff0c;我是 青松 &#xff01; &#x1f308; 自小刺头深草里&#xff0c;而今渐觉出蓬蒿。 NLP Github 项目…

vscode的一些实用操作

1. 焦点切换(比如主要用到使用快捷键在编辑区和终端区进行切换操作) 2. 跳转行号 使用ctrl g,然后输入指定的文件内容&#xff0c;即可跳转到相应位置。 使用ctrl p,然后输入指定的行号&#xff0c;回车即可跳转到相应行号位置。

OAI 平台 4G(LTE)基站 、终端、核心网 端到端部署实践(一)

本系列文章,基于OAI LTE代码搭建端到端运行环境,包含 eNB,EPC,UE三个网元。本小节先介绍系统总体架构,硬件平台及驱动安装方法。 1. Overview 系统总体架构如下图所示。 2 Machine setup 2.1 Machine specs Distributor ID: Ubuntu Description: Ubuntu 18.04.5 LTS…

Linux环境Docker使用代理推拉镜像

闲扯几句 不知不觉已经2月中了&#xff0c;1个半月忙得没写博客&#xff0c;这篇其实很早就想写了&#xff08;可追溯到Docker刚刚无法拉镜像的时候&#xff09;&#xff0c;由于工作和生活上的事比较多又在备考软考架构&#xff0c;拖了好久…… 简单记录下怎么做的&#xf…

基于TI的TDA4高速信号仿真条件的理解 4.6

Application Note 《Jacinto7 AM6x, TDA4x, and DRA8x High-Speed Interface Design Guidelines》 4.6 Reviewing Simulation Results检查仿真结果 The results generated by the channel simulations outlined in the preceding sections are compared against an eye mask s…

unity学习46:反向动力学IK

目录 1 正向动力学和反向动力学 1.1 正向动力学 1.2 反向动力学 1.3 实现目标 2 实现反向动力 2.1 先定义一个目标 2.2 动画层layer&#xff0c;需要加 IK pass 2.3 增加头部朝向代码 2.3.1 专门的IK方法 OnAnimatorIK(int layerIndex){} 2.3.2 增加朝向代码 2.4 …

DeepSeek 和 ChatGPT 在特定任务中的表现:逻辑推理与创意生成

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux网络编程 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 ​ Linux网络编程笔记&#xff1a; https://blog.cs…

DAY07 Collection、Iterator、泛型、数据结构

学习目标 能够说出集合与数组的区别数组:1.是引用数据类型的一种2.可以存储多个元素3.数组的长度是固定的 int[] arr1 new int[10]; int[] arr2 {1,2,3};4.数组即可以存储基本类型的数据,又可以存储引用数据类型的数据int[],double[],String[],Student[]集合:1.是引用数据类…

ls命令的全面参数解析与详尽使用指南

目录 ls 命令的所有参数及含义 ls -a 命令详解 ls -A 命令详解 ls -b 命令详解 ls -C 命令详解 ls -d 命令详解 ls -f 命令详解 ls -F 命令详解 ls -G 命令详解 ls -h 命令详解 ls -H 命令详解 ls -i 命令详解 ls -I 命令详解 ls -l 命令详解 ls -L 命令详解 l…

【Spring+MyBatis】_图书管理系统(中篇)

【SpringMyBatis】_图书管理系统&#xff08;上篇&#xff09;-CSDN博客文章浏览阅读654次&#xff0c;点赞4次&#xff0c;收藏7次。&#xff08;1&#xff09;当前页的内容records&#xff08;类型为List&#xff09;&#xff1b;参数&#xff1a;userNameadmin&&pas…