前言
做全屏的需要时,因为进度条会从半屏背景下的「根本不可能曝光」,变成全屏场景下「高频曝光」,所以须要打造一个丝滑、高可用的进度条,想当初我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 不满足「高可用」指标之前,咱们先思考一下,满足什么样的条件的进度条,才能够算是「高可用」?
我想出四个指标:
- UI可高度定制
- 晦涩的回调动画
- 可定制的响应范畴
- 响应手势,且无卡顿问题
其中 UISlider 可满足其中 3 和 4,因为 UISlider 是零碎提供的组件,「UI可高度定制」这条必定不满足。
且 UISlider 对于动画的解决不够弱小,在视频播放的场景下,视频播放器会定时高频的回调视频播放进度,更新进度的动画要足够晦涩,但实际上应用 UISlider 的成果是上面这样的:
所以 UISlider 不满足 第2点:「晦涩的回调动画」,而视频号场景下,视频进度回调更新进度条进度是高曝光的场景,肯定要把这个动画做得足够晦涩。
在这样的背景下,放弃 UISlider ,自定义进度条是惟一的抉择。
二、定制一份「高可用」进度条
Tips:BNCommonProgressBar
是咱们定制的进度条的类名,首先先看一下BNCommonProgressBar
实现的成果:
BNCommonProgressBar
设计与需要绝对应:
- 指标:UI可高度定制 --> 计划:自定义UI
- 指标:晦涩的回调动画 --> 计划:动画解决
- 指标:可定制的响应范畴 --> 计划:手势范畴解决
- 指标:响应手势,且无卡顿问题 --> 计划:拖拽手势解决,卡顿问题解决
所以 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 有更高的自定义水平。
且如果这些接口不够应用,你能够间接在BNCommonProgressBar
初始化办法中增加相应控件和组件,实现定制化,相对不会影响到 进度条动画/手势 等性能,实现性能隔离。
(二)回调进度解决
1. 问题剖析
为何触发暂停后,进度条不能立刻进行,而是滑动一段距离能力停下呢?
通过看进度条实现的代码,我发现了其中的端倪:
「进度条的地位」是通过「播放器的进度定时回调」来变更的
播放器大概每隔0.25秒会触发一次回调办法,通知进度条下个0.25秒应该挪动到哪个地位。
进度条为了实现进度变更的顺滑,采纳了[UIView animationWithDuration:]动画。
那么问题就来了:
如果在第2秒时,播放器回调通知了进度条下个0.25秒的地位,接着触发了[UIView animationWithDuration:0.25]的动画,
如果用户在2.01秒点击了暂停,如果这时不对动画做暂停的操作,进度条就会再挪动完剩下的 (0.25 - 0.01)秒的动画,也就呈现了暂停也滑动的体现。
解决这个问题最直观有两个计划:
计划一:减少播放器回调的频率
当频率足够高时,[UIView animationWithDuration:]的duration距离也会变小,那么暂停仍滑行的体现就会削弱
这个计划有两个问题:
- 减少播放器回调频率,只是削弱滑行的体现,但并没有真正解决滑行的问题。当同样的进度条援用到iPad上后,进度条会变长,那么问题仍会裸露
- 单纯减少播放器回调只是为了解决滑行问题,老本太高且没有必要。
计划二:暂停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:
。
**这个公众号会继续更新技术计划、关注业内技术动向,关注一下老本不高,错过干货损失不小。
↓↓↓**