打造高可用iOS进度条
前言
做全屏的需求时,因为进度条会从半屏背景下的「基本不可能曝光」,变成全屏场景下「高频曝光」,所以需要打造一个丝滑、高可用的进度条,想当初我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)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间隔也会变小,那么暂停仍滑行的表现就会减弱
这个方案有两个问题:
增加播放器回调频率,只是减弱滑行的表现,但并没有真正解决滑行的问题。当同样的进度条引用到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:
。
这个公众号会持续更新技术方案、关注业内技术动向,关注一下成本不高,错过干货损失不小。 ↓↓↓
打造高可用iOS进度条相关推荐
- 关闭删库跑路的后门,打造高可用的MySQL
0 MySQL HA/Scalability 如何关上"删库跑路"的后门,维护我们的数据安全呢? 数据是当今Web,移动,社交,企业和云应用程序的流行货币.确保数据始终可用是任何组 ...
- hybris mysql_利用 AWS 打造高可用 SAP Hybris 系统
概述: SAP Hybris是整个e-commerce 领域的领先系统解决方案,许多客户在选择打造自己的e-commerce系统时,都会考虑选择SAP Hybris. 这篇blog将会介绍如何利用AW ...
- iOS进度条 自定义圆角 UIProgressView
进度条总结,自定义圆角大小,多种方法定义圆角大小 //实例化一个进度条,有两种样式,一种是UIProgressViewStyleBar一种是UIProgressViewStyleDefault,几乎无 ...
- 如何用 javascript 做一个高逼格的进度条
可能你发现了本站顶部的进度条,它是如何实现的呢?下面一起来看. 页面进度条展示的是资源下载的进度,通常在页面上加上进度条,可以缓解用户的等待焦虑,也提升了网站的逼格. 前端进度条实现 在前端,实现网页 ...
- Flash打造美女影片指导进度条
我们制作一个好看的影片导入进度条. 先看效果. [img]/Files/BeyondPic/2007-6/5/20070603234254_01.gif[/img] 准备一幅图片: [img]/Fil ...
- Web前端 | 进度条 | 动态进度条 | IOS进度条 | 仿IOS进度条
一.Html代码 <!DOCTYPE html> <html lang="en"> <head><meta charset="U ...
- mysql主主复制+keepalived 打造高可用mysql集群
为了响应公司需求,打造出更安全的mysql集群,能够实现mysql故障后切换,研究了几天终于有了成果,一起分享一下. 首先介绍一下这套集群方案实现的功能 1.mysql服务器故障后自动转移,修好后自动 ...
- ios进度条Demo一个
一个很简单的Dmo.就拿出来分享一下. 一个简单的阴影效果 _progressView.frame = CGRectMake(size.width * progress-size.width, H_H ...
- Python-Flask构建微信小程序订餐系统-Flask打造高可用flask mvc框架-08
转载于:https://www.cnblogs.com/Paul-watermelon/articles/10759597.html
- 微信支付:如何打造移动支付时代的高可用收银系统?
作者|商户运营开放团队 编辑|小智 移动支付时代,越来越多的人习惯于不带现金出门,许多支付场景只需要掏出手机就能完成.正因为如此,收银系统的可用性问题也越来越重要.如何打造移动支付时代的高可用收银系统 ...
最新文章
- 基于自动驾驶车辆的激光雷达与摄像头之间的在线标定算法
- java 基本的数据类型_Java的基本数据类型介绍
- html制作选择题题库,HTML与网页制作测试题库
- 二十一、Hadoop学记笔记————kafka的初识
- linux版车机安装步骤,RedHat Linux 9.0的安装(详细图解安装过程)
- Hbase Compaction 源码分析 - CompactSplitThread 线程池选择
- 迷你世界显示未连接服务器成功,迷你世界登录未成功是什么意思 | 手游网游页游攻略大全...
- 水文特点是什么意思_水文监测仪器设备简介
- 程序设计工程师c语言,《C语言程序设计》自学百问.doc
- Python对命令提示符cmd以及操作系统的一些操作
- 机器学习--支持向量机(四)SMO算法详解
- 网格搜索的原理以及实战以及相关API(GridSearchCV)
- PCworld 101个 fabulous Freebies
- 境外上市,一个绝非遥不可及的梦想!
- 以太坊--Uniswap分享
- 面试请假攻略,不用裸辞也能找到工作
- 读--《谁动了我的奶酪》
- 易语言大漠插件模块制作后台找字FindStrFastEx
- Photoshop 去掉图片上的文字的几个方法
- Metronic 对话 chat
热门文章
- UI 即 User Interface( 用户界面 ) 的简称
- colorbox加载ajax调用的html页面,jQuery的弹出窗口 ColorBox
- 腾讯云“黑石”真相——“物理私服”
- 南方证券超强版 下载
- 这些那些非代码之罪的坑
- N-gage QD等S60 V1.2机型C盘减肥80K的办法(超越3600KB)
- VMware ESXi-虚拟化平台的搭建
- dede服务器建站_新手搭建DeDecms织梦网站的详细操作流程
- 麻省理工大学计算机中心,Massachusetts Institute of Technology 麻省理工大学
- python暑假培训班