前言

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

今天把这个进度条的架构、设计逻辑和踩苹果13过的坑都整理一下开源矿工

本文涉及的代码已开源至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 的效果是下面这样的:

【iOS手把手实战系列】打造一个顺滑的进度条竟然有那么多坑...附源码

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

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

二、定制一份「高可用」进度接口crc错误计数

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

【iOS手把手实战系列】打造一个顺滑的进度条竟然有那么多坑...附源码

B开源软件NCo开源阅读mmonProgre动画片少儿小猪佩奇ssBar 设计与需求相对应:

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

所以开源中国 BNCommonProgressBar 的设计也就动画制作软件分为 4个模块。

动画电影一)自定义UI

BNCommonProgressBar初始化方法为:

- (instancetype)initWithFrame:(CGRect)frame
                    barHeight:(CGFloat)progressBarHeight
                    dotHeight:(CGFloat)dotHeight
                 defaultColor:(UIColor *)defaultColor
              inProgressColor:(UIColor *)inProgressColor
                    dragColor:(UIColor *)dragColor
                 cornerRadius:(CGFloat)cornerRadius
         progressBarIconImage:(UIImage *)progressBarIconImage
         enablePanProgressIcon:(BOOL)enablePanProgressIcon;

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

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

(二)回调进度处理

1. 问题分析

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

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

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

播放器大约每隔0.苹果x25秒会触发一次回调接口crc错误计数开源法,告诉进度接口和抽象类的区别条下个0.25秒应该移动到哪个位置。

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

那么问题就来了:

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

如果用户在2.01秒点击了暂停,如果这时不对动画做暂停的操作,进度条就会再移动完剩下的 (0.苹果范冰冰25 – 0.01)秒的动画,也就出现了暂苹果8plus停也滑动的表现。

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

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

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

这个方案有两个问题:

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

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

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

下面我们就围绕着暂动画制作软件停Cor动画大放映eAnimation动画的方动画制作软件案,引入和补充一架构工程师些关于Layer动画的知识点。

(1)实现过程

方案二有个直观的步骤:

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

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

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

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

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

可以直接记录view.x吗?

实际上是不行的,如果我架构是什么意思们将 [UIView animationWithDuration:]发生前 view.x 记录为 A,动画完成后 view.x 记录为 C,动画过程中记录接口crc错误计数为 B。

____I________I_______I___
  起点A     暂停B     终点C

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

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

这是为什么呢?

CALayer图层树

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

CALayer 和 UIView 一样存在着一个层级树状结构,称之为图层树(Layer Tr架构图模板ee),也可以叫 模型树动画片小猪佩奇(Model Tree)。

【iOS手把手实战系列】打造一个顺滑的进度条竟然有那么多坑...附源码

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

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

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

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

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

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

什么开源中国是mode苹果lLa动画电影ye开源代码网站githubr?

modelLayer 实际上就是承载着layer终态的各种数据,我们开发过程中给layer的各种参数赋值,实际上也就是苹果12给layer.m动画头像odelLayer赋值。

也即:vi动画片汪汪队ew.laye接口英文r == view.layer.modelLayer。

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

架构是什么意思么是p接口测试resentationLayer?

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

所以在执行core ani开源矿工mation动画中,presen苹果xtationLayer 是时刻变化的,但modelLayer是不动画头像会变的。

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

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

停止core anima苹果8tion动画有很多种方式,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也采纳了此协动画大放映议,用来实现动画开源众包的时间系统.)

beginTi动画片汪汪队me

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

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

timeOffset

这个timeOffset可能是这几个属性中比较难理解的一个,官方的文档也没有讲的很清楚. local time也分成两种:一种是active local time 一种是basic local苹果 time. timeOffset接口和抽象类的区别则是active local tim开源阅读app下载安装e的偏移量. 你将一个动画开源看作一个环,timeOffset改变的其实是动画在环内的起点,比如一个duration为5秒的动画,架构图模板将timeOffs开源代码网站githubet设置为2(或者7,模5为2),那么动画的运行则是从原来的2秒开始到5秒,接着再0秒到2秒,完成一次动画.

speed

spee动画片汪汪队d属性用于设置当前对象的时间流苹果官网相对于父级对象时间流的开源节流流逝速度,比如一个动画beginTime是0,苹果7但是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:wi苹果7thEvent:方法进行处理,可以将响应范围使用宏进行界定,也可由业务层传入,这里默认上下左右增加 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:


这个公众号会开源是什么意思持续更新技术方案、关注业内技开源中国术动向,关注一下成本不高,错过干货损架构是什么意思失不小。 ↓↓↓

【iOS手把手实战系列】打造一个顺滑的进度条竟然有那么多坑...附源码

本文由博客一文多发平台 OpenWrite 发布!