打造高可用iOS进度条

article/2025/10/3 21:55:16

前言

做全屏的需求时,因为进度条会从半屏背景下的「基本不可能曝光」,变成全屏场景下「高频曝光」,所以需要打造一个丝滑、高可用的进度条,想当初我Debug到凌晨4点,就是为了解决暂停后进度条的动画问题。

今天把这个进度条的架构、设计逻辑和踩过的坑都整理一下。

本文涉及的代码已开源至Github:打造高可用进度条

接口介绍

BNCommonProgressBar.h// 变更进度,animateWithDuration是传入动画时间
- (void)setValue:(CGFloat)value;
- (void)setValue:(CGFloat)value animateWithDuration:(NSTimeInterval)duration time:(NSTimeInterval)time;
- (void)setValue:(CGFloat)value animateWithDuration:(NSTimeInterval)duration completion:(void (^__nullable)(BOOL finished))completion;// 重置所有状态,会将进度重置到0
- (void)reset;// 暂停动画
- (void)pauseAnimation;
// 恢复动画
- (void)resumeAnimation;
// 清理动画状态,手动拖拽时先清理动画状态
- (void)removeProgressAnimation;

一、为什么 UISlider 不满足「高可用」的目标?

在阐述 UISlider 不满足「高可用」目标之前,我们先思考一下,满足什么样的条件的进度条,才可以算是「高可用」?

我想出四个目标:

    1. UI可高度定制
    1. 流畅的回调动画
    1. 可定制的响应范围
    1. 响应手势,且无卡顿问题

其中 UISlider 可满足其中 3 和 4,因为 UISlider 是系统提供的组件,「UI可高度定制」这条肯定不满足。

且 UISlider 对于动画的处理不够强大,在视频播放的场景下,视频播放器会定时高频的回调视频播放进度,更新进度的动画要足够流畅,但实际上使用 UISlider 的效果是下面这样的:

所以 UISlider 不满足 第2点:「流畅的回调动画」,而视频号场景下,视频进度回调更新进度条进度是高曝光的场景,一定要把这个动画做得足够流畅。

在这样的背景下,放弃 UISlider ,自定义进度条是唯一的选择。

二、定制一份「高可用」进度条

Tips:BNCommonProgressBar是我们定制的进度条的类名,首先先看一下BNCommonProgressBar实现的效果:

BNCommonProgressBar 设计与需求相对应:

    1. 目标:UI可高度定制 --> 方案:自定义UI
    1. 目标:流畅的回调动画 --> 方案:动画处理
    1. 目标:可定制的响应范围 --> 方案:手势范围处理
    1. 目标:响应手势,且无卡顿问题 --> 方案:拖拽手势处理,卡顿问题处理

所以 BNCommonProgressBar 的设计也就分为 4个模块。

(一)自定义UI

BNCommonProgressBar初始化方法为:

- (instancetype)initWithFrame:(CGRect)framebarHeight:(CGFloat)progressBarHeightdotHeight:(CGFloat)dotHeightdefaultColor:(UIColor *)defaultColorinProgressColor:(UIColor *)inProgressColordragColor:(UIColor *)dragColorcornerRadius:(CGFloat)cornerRadiusprogressBarIconImage:(UIImage *)progressBarIconImageenablePanProgressIcon:(BOOL)enablePanProgressIcon;

允许业务层配置进度条高度、进度圆点高度、默认颜色、处于进度拖拽时的颜色、是否允许拖拽等,相比 UISlider 有更高的自定义程度。

且如果这些接口不够使用,你可以直接在BNCommonProgressBar初始化方法中添加相应控件和组件,实现定制化,绝对不会影响到 进度条动画/手势 等功能,实现功能隔离。

(二)回调进度处理

1. 问题分析

为何触发暂停后,进度条不能立即停止,而是滑动一段距离才能停下呢?

通过看进度条实现的代码,我发现了其中的端倪:

「进度条的位置」是通过「播放器的进度定时回调」来变更的

播放器大约每隔0.25秒会触发一次回调方法,告诉进度条下个0.25秒应该移动到哪个位置。

进度条为了实现进度变更的顺滑,采用了[UIView animationWithDuration:]动画。

那么问题就来了:

如果在第2秒时,播放器回调告诉了进度条下个0.25秒的位置,接着触发了[UIView animationWithDuration:0.25]的动画,

如果用户在2.01秒点击了暂停,如果这时不对动画做暂停的操作,进度条就会再移动完剩下的 (0.25 - 0.01)秒的动画,也就出现了暂停也滑动的表现。

解决这个问题最直观有两个方案:

方案一:增加播放器回调的频率

当频率足够高时,[UIView animationWithDuration:]的duration间隔也会变小,那么暂停仍滑行的表现就会减弱

这个方案有两个问题:

  1. 增加播放器回调频率,只是减弱滑行的表现,但并没有真正解决滑行的问题。当同样的进度条引用到iPad上后,进度条会变长,那么问题仍会暴露

  2. 单纯增加播放器回调只是为了解决滑行问题,成本太高且没有必要。

方案二:暂停CoreAnimation进行中的动画

下面我们就围绕着暂停CoreAnimation动画的方案,引入和补充一些关于Layer动画的知识点。

(1)实现过程

方案二有个直观的步骤:

  • a. 当视频暂停时,记录暂停那一刻 进度条的位置

  • b. 然后停止进度条的动画

  • c. 等到恢复播放时,再从上次记录的位置重新恢复动画。

a. 当视频暂停时,记录暂停那一刻 进度条的位置

首先问题是:应该记录进度条view的哪个属性呢?

可以直接记录view.x吗?

实际上是不行的,如果我们将 [UIView animationWithDuration:]发生前 view.x 记录为 A,动画完成后 view.x 记录为 C,动画过程中记录为 B。

____I________I_______I___起点A     暂停B     终点C

你会发现,只要动画开始了,无论动画是否结束,你通过 view.x 访问到的总是 C,而非动画过程暂停那一刻的位置 B。

甚至如果你对view.layer.frame进行KVO的监测,你会发现在动画变更过程中,KVO并没有回调。

这是为什么呢?

CALayer图层树

我们都知道,UIView是对CALayer的一个封装,CALayer类在概念上和UIView类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView最大的不同是CALayer不处理用户的交互。

CALayer 和 UIView 一样存在着一个层级树状结构,称之为图层树(Layer Tree),也可以叫 模型树(Model Tree)。

这三种图层树有什么作用呢?说到有啥作用,就不得不提Core Animation 核心动画了。因为这三个图层在核心动画中才能显示出它们的特点和用处。下面是官方文档的说明:

  • 模型图层树 中的对象是应用程序与之交互的对象。此树中的对象是存储任何动画的目标值的模型对象。每当更改图层的属性时,都使用其中一个对象。

  • 表示图层树 中的对象包含任何正在运行的动画的飞行中值。层树对象包含动画的目标值,而表示树中的对象反映屏幕上显示的当前值。您永远不应该修改此树中的对象。相反,您可以使用这些对象来读取当前动画值,也许是为了从这些值开始创建新动画。

  • 渲染图层树 中的对象执行实际动画,并且是Core Animation的私有动画。

也就是说,图层树中我们开发过程中可以实际用到的有两个属性:modelLayer (模型图层)、presentationLayer(表现图层)。

(渲染图层在CALayer没有提供直接的属性给我们使用,是core Animation私有的)

什么是modelLayer?

modelLayer 实际上就是承载着layer终态的各种数据,我们开发过程中给layer的各种参数赋值,实际上也就是给layer.modelLayer赋值。

也即:view.layer == view.layer.modelLayer。

因为modelLayer是我们在进行动画时设定好的最终值,所以在动画执行过程中,对view.layer.frame进行KVO监测,是不会有值的变更的。

什么是presentationLayer?

presentationLayer 是我们的主角,presentationLayer指的也就是 屏幕上实时展示的图层的layer ,在core animation 动画中,可以通过这个属性,获取动画过程中每个时刻动画图层的数据,这样如果在动画过程中需要做什么处理,就可以动态的获取layer上相关的数据了。

所以在执行core animation动画中,presentationLayer 是时刻变化的,但modelLayer是不会变的。

presentationLayer有诸多用途,比如视频中的滚动弹幕如果是使用layer做动画的,当弹幕正在滚动时,你需要点击它以处理需要做的事情,这时候你就会需要presentationLayer。再结合hintTest方法来做判断:

[self.layer.presentationLayer hitTest:point] //判断是不是你点击的哪个弹幕
b. 停止进度条的动画

停止core animation动画有很多种方式,layer.removeAllAnimations 就是其中一种。

但layer.removeAllAnimations并不能实现我们预期的效果,举例:

____I________I_______I___起点A     暂停B     终点C

在暂停B点,调用removeAllAnimations,动画是会停止,但进度会直接跳到最终态C,而非停在B,所以我们需要的是可以 pauseAnimation,而非removeAnimation的操作。

虽然CALayer没有提供pauseAnimation的接口,但我们可以通过CALayer的时间模型来实现pause的效果。

CAMediaTiming协议

CAMediaTiming协议的内容不多,头文件我罗列于此。

@protocol CAMediaTiming@property CFTimeInterval beginTime;
@property CFTimeInterval duration;
@property float speed;
@property CFTimeInterval timeOffset;
@property float repeatCount;
@property CFTimeInterval repeatDuration;
@property BOOL autoreverses;
@property(copy) CAMediaTimingFillMode fillMode;@end

CALayer实现了CAMediaTiming协议. CALayer通过CAMediaTiming协议实现了一个有层级关系的时间系统. (除了CALayer,CAAnimation也采纳了此协议,用来实现动画的时间系统.)

beginTime

无论是图层还是动画,都有一个时间线Timeline的概念,他们的beginTime是相对于父级对象的开始时间. 虽然苹果的文档中没有指明,但是通过代码测试可以发现,默认情况下所有的CALayer图层的时间线都是一致的,他们的beginTime都是0,绝对时间转换到当前Layer中的时间大小就是绝对时间的大小.所以对于图层而言,虽然创建有先后,但是他们的时间线都是一致的(只要不主动去修改某个图层的beginTime),所以我们可以想象成所有的图层默认都是从系统重启后开始了他们的时间线的计时.

但是动画的时间线的情况就不同了,当一个动画创建好,被加入到某个Layer的时候,会先被拷贝一份出来用于加入当前的图层,在CA事务被提交的时候,如果图层中的动画的beginTime为0,则beginTime会被设定为当前图层的当前时间,使得动画立即开始.如果你想某个直接加入图层的动画稍后执行,可以通过手动设置这个动画的beginTime,但需要注意的是这个beginTime需要为 CACurrentMediaTime()+延迟的秒数,因为beginTime是指其父级对象的时间线上的某个时间,这个时候动画的父级对象为加入的这个图层,图层当前的时间其实为[layer convertTime:CACurrentMediaTime() fromLayer:nil],其实就等于CACurrentMediaTime(),那么再在这个layer的时间线上往后延迟一定的秒数便得到上面的那个结果.

timeOffset

这个timeOffset可能是这几个属性中比较难理解的一个,官方的文档也没有讲的很清楚. local time也分成两种:一种是active local time 一种是basic local time. timeOffset则是active local time的偏移量. 你将一个动画看作一个环,timeOffset改变的其实是动画在环内的起点,比如一个duration为5秒的动画,将timeOffset设置为2(或者7,模5为2),那么动画的运行则是从原来的2秒开始到5秒,接着再0秒到2秒,完成一次动画.

speed

speed属性用于设置当前对象的时间流相对于父级对象时间流的流逝速度,比如一个动画beginTime是0,但是speed是2,那么这个动画的1秒处相当于父级对象时间流中的2秒处. speed越大则说明时间流逝速度越快,那动画也就越快.比如一个speed为2的layer其所有的父辈的speed都是1,它有一个subLayer,speed也为2,那么一个8秒的动画在这个运行于这个subLayer只需2秒(8 / (2 * 2)).所以speed有叠加的效果.

有上面三个属性,我们就可以实现 pause 和 resume 的操作,相关代码如下:

#pragma mark 暂停和恢复CALayer的动画
- (void)pauseLayer:(CALayer *)layer {CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];// 让CALayer的时间停止走动layer.speed = 0.0;// 让CALayer的时间停留在pausedTime这个时刻layer.timeOffset = pausedTime;
}- (void)resumeLayer:(CALayer *)layer {CFTimeInterval pausedTime = layer.timeOffset;// 1. 让CALayer的时间继续行走layer.speed = 1.0;// 2. 取消上次记录的停留时刻layer.timeOffset = 0.0;// 3. 取消上次设置的时间layer.beginTime = 0.0;// 4. 计算暂停的时间(这里也可以用CACurrentMediaTime()-pausedTime)CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;// 5. 设置相对于父坐标系的开始时间(往后退timeSincePause)layer.beginTime = timeSincePause;
}

上面方法中使用到的 CACurrentMediaTime(),也就是所谓的 马赫时间,它是CoreAnimation上的一个全局时间的概念。

马赫时间在设备上所有进程都是全局的--但是在不同设备上并不是全局的--不过这已经足够对动画的参考点提供便利了。

这个函数返回的值其实无关紧要(它返回了设备自从上次启动后的秒数,并不是你所关心的),它真实的作用在于对动画的时间测量提供了一个相对值。注意当设备休眠的时候马赫时间会暂停,也就是所有的CAAnimations(基于马赫时间)同样也会暂停。

(三)手势范围处理

BNCommonProgressBar中针对hitTest:withEvent:方法进行处理,可以将响应范围使用宏进行界定,也可由业务层传入,这里默认上下左右增加 BNResponseWidHeight/2 的响应范围。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {CGRect respRect = CGRectMake(- BNResponseWidHeight/ 2, - BNResponseWidHeight / 2, self.width + BNResponseWidHeight, self.height + BNResponseWidHeight);if (CGRectContainsPoint(respRect, point)) {return self;}return [super hitTest:point withEvent:event];
}

(四)拖拽手势处理,卡顿问题处理

拖拽手势处理的代码在onPanProgressIcon:


这个公众号会持续更新技术方案、关注业内技术动向,关注一下成本不高,错过干货损失不小。 ↓↓↓


http://chatgpt.dhexx.cn/article/9EgrClVj.shtml

相关文章

html ios视频播放器,iOS 视频播放器(整理)

1、WMPlayer //WMPlayer视频播放器,AVPlayer的封装,继承UIView,想怎么玩就怎么玩。支持播放mp4、m3u8、3gp、mov,网络和本地视频同时支持。全屏和小屏播放同时支持。 cell中播放视频,全屏小屏切换自如。(推荐5颗&#…

android+仿ios+音乐播放器,iOS简单的音乐播放器(仿QQ音乐)

AVPlayer实现基本的播放,暂停,上一首,下一首,调节音量,调节进度等,正在学习的新人可以看下,有什么不足可以互相学习,谢谢支持 qq音乐.gif 这个是我写的一个简单的低仿QQ音乐, 如果你也喜欢听音乐的话, 快来自己制作一个吧! 下面我们来实现, 首先我们用单例写的音乐管理…

iOS 视频播放器开发

需求设计 做一个小学生教育辅导视频播放器。 参考小猿搜题视频播放器 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L0GsyFSt-1675164972791)(https://tva1.sinaimg.cn/large/008vxvgGgy1h9xk4fm5xfj31sx0u0mz0.jpg)] [外链图片转存失败,源站可…

iOS播放器SDK-基于FFmpeg解码OpenGL渲染-CYPlayer

CYPlayer基于Objective-C编写,支持RTMP和HTTP协议的播放流,如果需要smb协议,可以自行修改CYFFmpeg兼容samba协议即可。 1.支持动态帧率控制,适配各种性能的机型,最高支持1080p60fps; 2.动态内存控制,防止…

iOS播放器常用功能

iOS播放器常用功能 强制横屏 在播放器中常见强制横屏,例如,如下这种: OC实现代码如下: if ([[UIDevice currentDevice] respondsToSelector:selector(setOrientation:)]) {SEL selector NSSelectorFromString(&…

IOS开发之——音乐播放器-播放器页面(05)

一 概述 播放器页面Storyboard点击列表Cell时,播放器页面show弹出播放器页面按钮对应的功能 二 播放器页面Storyboard 2.1 Storyboard界面 2.2 界面说明 界面分类:顶部View和底部View两部分顶部View:歌曲大图、名字背景(歌曲名和歌手名)、…

西瓜视频 iOS 播放器技术重构

动手点关注 干货不迷路 👆 播放器简介 播放器是西瓜视频等视频类 App 最主要的业务场景,也是最主要的流量入口,其承载包括下层基础播放,上层的各种播放业务:状态栏、弹幕、音量、亮度、评论、点赞、进度、倍速、清晰度…

iOS系统自带的视频播放器

简述:MPMoviePlayerController,MPMoviePlayerViewController,AVPlayerViewController三种视频播放器的讲解1.MPMoviePlayerController效果:MPMoviePlayerController 密码: yam8MPMoviePlayerController&#xff0c;在MediaPlayer框架中 #import <MediaPlayer/MediaPlayer.h&…

iOS 超好用的本地视频播放器推荐!

本地播放器作为日常生活中不可或缺的一款工具类APP&#xff0c;Windows、Android等平台不乏一些功能与体验兼优的产品&#xff0c;但 iOS 平台的用户就没有那么幸运了&#xff0c;优秀的产品凤毛麟角&#xff0c;且多数收费。 这源于 iOS 平台的特殊性&#xff0c;完美支持各种…

iOS视频播放器之ZFPlayer剖析

1、引言 本文主要针对ZFPlayer的功能实现来剖析&#xff0c;以及总结一下大家遇到的问题和解决方案 首先ZFPlayer现在拥有的功能&#xff1a; 支持横、竖屏切换&#xff0c;在全屏播放模式下还可以锁定屏幕方向 支持本地视频、网络视频播放 支持在TableviewCell播放视频 左侧1/…

996程序员办公室猝死?公司:没死,继续上班了

程序员头条 报道 网传 996 程序员办公室猝死&#xff0c;公司回应&#xff1a;系因低血糖晕倒 日前&#xff0c;一则“996程序员办公室猝死”的视频在网络传播。根据视频显示&#xff0c;一名男子扶着办公桌栏短暂逗留之后&#xff0c;直挺挺的摔倒在地面。 事发后&#xff0c;…

网传京东37岁程序员在工位猝死。当事人:我还没死,还能加班!

编程牛人整理 近日微信群里疯传&#xff0c;一男子在工位突然倒地的图片与视频&#xff0c;随后&#xff0c;有自媒体称&#xff0c;这位倒地的员工系京东程序员疑似因常年夜班在工位猝死。 3月25日&#xff0c;网传消息称&#xff0c;该员工今年37岁&#xff0c;在加班过程中猝…

35岁程序员被公司辞退,生活压力太大痛哭,中年危机如何自救?

多数人都喜欢安逸的生活&#xff0c;尤其是随着年龄的增长&#xff0c;很多人都希望工作和生活趋于稳定&#xff0c;不愿意再让生活有很大的变动。可是&#xff0c;当达到一定的年龄时&#xff0c;危机还是存在的。 如今已经35岁的杰哥&#xff0c;是一个典型的理工男&#xf…

秒啊!程序员防猝死指南来了!

‍‍ 作者 | 咏春警告的胖虎 来源 | golang小白成长记&#xff08;ID&#xff1a;golangxbczj&#xff09; 过年之前&#xff0c;跟我可爱的小侄子通了个电话&#xff0c;上来就说&#xff0c;"叔叔你头发怎么变少了"&#xff0c;我很痛心&#xff0c;我的小侄子&…

程序员工作猝死给公司造成损失可以找其父母追讨吗?

从 996.icu 开始&#xff0c;996开始被热议&#xff0c;马巴巴说&#xff1a;“996 是福报”&#xff0c;东哥说&#xff1a;“81168”。然后 90 后乃至 00 后被推出来说&#xff1a;“这届真难带&#xff01;”。我一直认为网上有个段子说得好。  ☠ 月薪5W&#xff1a;996就…

程序员离职事件始末

吴小胖第10次推送 阅读时间预计3分钟~ 这是我毕业后的第一份工作... 面试时&#xff0c;HR小姐姐告诉我... 然鹅...我入职之后才发现&#xff1a; 对标阿里的只有加班强度 对标华为的只有狼性文化 对标百度的&#xff0c;额&#xff0c;没有对标百度 同事们有的住在海淀区、有的…

为什么程序猿996会猝死,而企业家007却不会?

想和吴小胖一起工作吗&#xff1f; 内推邮箱【shen_baili163.com】&#xff0c;响应P99非常小 其实&#xff0c;也可以关注公众号&#xff0c;不取关的那种哦 然后留言&#xff0c;我会主动联系你哟 后面还会继续更新Apache Dubbo源码分析和ElasticSearch的内容哦&#xff0c;大…

唉!一 28 岁程序员因新冠离世。。。

上一篇&#xff1a;为什么总是闹离职的员工没走&#xff0c;平时不吭声的员工却突然离职&#xff1f; 2022年12月19日&#xff0c;微信公众号万户楼台 发布《西安28岁程序员重症病危》的内容。 病人名字为郭晓桐&#xff08;94年生&#xff09;&#xff0c;年龄28岁。 其没有基…

成年人的崩溃只在一瞬间,程序员凌晨三点写的代码竟被女友删了...

对于恋爱中的情侣来说&#xff0c;吵架是很正常的事情&#xff0c;就算是再怎么亲密&#xff0c;也难免会出现意见不合的时候。 吵架不可怕&#xff0c;可怕的是&#xff0c;受吵架情绪的影响&#xff0c;做出一些比较“极端”的事情。 之前某社交平台上一位女生吐槽自己的男…

违法?猝死?你肯定不知道程序员还有这些“高危”操作

全文共2975字&#xff0c;预计学习时长9分钟 图源&#xff1a;百度 10月24日&#xff0c;一段“996程序员猝死在1024程序员节”的视频在各大IT群疯传。不久 “程序员猝死”的消息被搬上了热搜。 图源&#xff1a;微博 看毕&#xff0c;坐在电脑面前的程序员们觉得自己的心跳仿佛…