仿写及比较标哥的iOS时钟动画

1、前言git

  之前看各类绚丽的UI特效动画代码,采用的方法是会先运行一篇,而后直接去看实现代码。初学时抱着瞻仰的态度去接触,去认识,是没有错的。可是在了解了像素、动画渲染机制,CoreAnimation API,推导过二维、三维的仿射矩阵以后,咱们能够改变阅读UI动画博文或者是源码的方式了。github

  Talk is cheap, show me the code——Linus Torvalds。编程

  大量的仿写;必定必定要多写——叶孤城__ 在CodeReview线下大会上的发言。架构

  最近安居客、猿题库、蘑菇街、滴滴都有在谈iOS客户端的架构设计,不少童鞋在说看不懂或者根本就是viper之类的话,是否是举重若轻不敢轻易评论。但只有经历过多人合做,没有统一架构规范,不断填充ViewController, 使得VC从几十行增加到千余行再拆分至几百行;经历过近百个VC类的各类产品跳转需求创(瞎)新(搞),才能了解Massive ViewController的痛和页面跳转逻辑cyclomatic complexity超量的难以承受吧。函数

2、仿写的UI动画结果比较性能

  原文连接:http://www.henishuo.com/clock-animation/动画

标哥博文提供的工程运行截图 笔者的工程运行截图

  从呈现效果的直观认识来看,质量是相近的;编码

  从UI美观上来看,标哥集中在核心功能编码,我有些注重无谓的美学外观,所以对指针和钟心的指针盖冒都作了路径绘制,看起来会漂亮一点么^^atom

  从运行性能上来看,CPU的消耗都是0,内存、动画流畅性等方面是差很少的spa

  从组件可用性来看,标哥固然不应浪费精力作这么个简单的组件,因此我提供的组件API仍是比较多的,提供了代码xib兼容初始化,钟表时间的设置,暂停,运行等,钟表时间值的手动KVO,表盘背景图的设置等,基本上有虚拟钟表的需求时,个人这个组件是能够直接拿来用的。

  从编码思路上看,标哥将现实世界问题直接转换到机器实质,好比直接指定指针动画的duration;而个人组件开发思路一直是搭建现实世界到机器世界的中间桥梁,这样任何现实世界的规律都能经过中间桥梁转换到工程方法和UI显示。任何运行状态都能经过中间桥梁映射到现实世界,被人类逻辑所理解。标哥的思路定然是高效的,但个人思路更贴近人类思惟。仍是那句话吧,编程之路法无定法,但由你本身选择。

3、UI与技术需求分析

  全部的需求分析和编码工做是在阅读标哥提供的源码Demo以前的,以锻炼我的独立分析问题、解决问题的能力。

  UI实现上,由于不提供交互,因此选择轻量级的CALayer,用到的OC类主要是UIView、CAShapeLayer、UIBezierPath。另外在中心盖帽的绘制上,我用了CAGradientLayer。

  逻辑实现上,个人思路是周期一秒钟后,人为去驱动钟表时间属性变化和UI更新,所以用到了NSTimer。这里NSTimer有retain cycle的问题,经常使用的解决方案有弱引用,中间代理,GCD Timer等。标哥选择了第一种,个人见解是我须要强执有我要用的东西,固然这也是从哲学思辨来考虑。所以,我用了中间代理这种方法,之前有写过,就直接拿来用了。在KVO的实现上,我使用了手动KVO,由于time属性提供给使用方用setter方法来设置更改,接入方确定不想观察到本身设置时的KVO,还得先移除,再添加。所以,我编码时setter方法时不发布变化信息,而是在钟表自动运行时time的改变提供手动KVO.

  其它须要注意的是,NSTimer的建立与提交须要消耗CPU,所以不要频繁的建立销毁,只在接入方设置更改当前时间时,更换Timer。

4、类设计与编码

  在其它语言中,有接口的概念但OC没有。那么如何面向接口编程呢,我想Protocol是一种可取的方法。在写一个类以前,若是有时间仍是要作一下接口设计比较好。示例以下:

@protocol HSClockViewProtocol <NSObject>
/**
 *  一个时钟与外界的通讯,就是它的时间。
 *  要有setter/getter, KVO-compliance
 */
@property (nonatomic, assign) NSTimeInterval time;
/**
 *  暂停时钟运行
 */
- (void) pause;
/**
 *  继续或者开始时钟运行
 */
- (void) work;

/**
 *  设置表盘背景图
 *
 *  @param image 表盘背景图,UIImage对象
 */
- (void) setDialBackgroundImage:(UIImage *) image;

@end

5、现实世界与机器世界的转换关系

  在虚拟时钟这个问题上仍是比较简单的,主要在于时间字符串或者Unix时间戳到三个指针的弧度角行向量的转换,代码以下:

/**
 *  时针、分针、秒针的弧度角(左手二维坐标系下,与X轴正方向的夹角。从屏幕外看,顺时针为增加方向)
 */
typedef struct HSClockHandRadian {
    double hourRadian;
    double minuteRadian;
    double secondRadian;
} HSClockHandRadian;

HSClockHandRadian HSRadianFromTimeInterval(NSTimeInterval time) {
    time += 8 * 60 * 60; //北京时间 +8
    NSInteger offsetIn12Hour = (NSInteger)time % (12 * 60 * 60); // 以12小时为周期时,偏移的秒数,时针
    NSInteger offsetIn1Hour = (NSInteger)time % (1 * 60 * 60); // 以1小时为周期时,偏移的秒数,分针
    NSInteger offsetIn1Minute = (NSInteger)time % (1 * 60); // 以1分钟为周期时,偏移的秒数,秒针
    
    HSClockHandRadian handRadian;
    handRadian.hourRadian = offsetIn12Hour * 1.0 / (12 * 60 * 60) * M_PI * 2- M_PI_2;
    handRadian.minuteRadian = offsetIn1Hour * 1.0  / (1 * 60 * 60) * M_PI * 2 - M_PI_2;
    handRadian.secondRadian = offsetIn1Minute * 1.0  / (1 * 60) * M_PI * 2 - M_PI_2;
    return handRadian;
}

HSClockHandRadian HSTimeFromTimeStr(NSString *timeStr) {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"yyyy-MM-dd hh:mm:ss";
    NSString *dateStr = [NSString stringWithFormat:@"1970-01-01 %@", timeStr];
    NSDate *date = [dateFormatter dateFromString:dateStr];
    NSTimeInterval timeStamp = [date timeIntervalSince1970];
    return HSRadianFromTimeInterval(timeStamp);
}

HSClockHandRadian HSTimeFromDate(NSDate *date) {
    NSTimeInterval timeStamp = [date timeIntervalSince1970];
    return HSRadianFromTimeInterval(timeStamp);
}

6、指针弧度角到仿射矩阵的变换

  二维中的平移、缩换、平面原点为圆心旋转、平面任何点为圆心旋转,三维中的平移、缩换、绕坐标轴旋转、绕任意轴旋转、透视等,都在于仿射矩阵的变化。笔者建议,仍是本身去把转换关系推导出来,所以不打算提供转换矩阵^^

  在这里提供几点思路和注意点:

  1.cor_new = cor_old * M,其中cor_new、cor_old均为行向量,一个是原值,一个是指望值,这两个咱们知道后,能够把仿射矩阵M推导出来。

  2.iOS在CA中采用与UIKit相同的左手坐标系,三维坐标系时Z轴向外。二维时从屏幕外看,顺时针为旋转角增加方向。三维时看向旋转轴的负方向,顺时针为旋转角的增加方向。实际上,二维时绕原点的旋转即绕Z轴旋转。

  3.绕任意轴旋转时,先将坐标系转换,使得旋转轴与一坐标轴重合,在此坐标系完成旋转后,再作坐标系逆转换。

  4.三维视效主要体如今透视点的设置上。通常设定下,人眼从屏幕外看动画,即透视点在z轴上变化。

  5.推导过程涉及到矩阵运算,相乘,求逆等;涉及到三角函数和差化积等。

7、工程中声明的私有属性、成员变量和私有方法

  关于在Extension里写私有属性仍是在implement后的花括号里写成员变量,唐巧大神有过论述,有兴趣的能够去看下唐巧的技术博客。私有方法是否在Extension里声明呢,个人见解是尽可能写一下,别人看你代码的时候可以迅速的知道你实现了哪些私有方法。代码示例以下:

@interface HSClockView() /** * 内部标识时钟是否在运行中 */ @property (nonatomic, assign, getter=isWorking) BOOL working; /** * 初始化当前时间,背景,指针, 供代码建立与xib建立共用 */
- (void) p_initClockView; /** * 初始化指针并返回 * * @param width 指针宽度 * @param height 指针高度 * @param tailLength 指针尾部长度 * @param tickLength 指针尖部长度 * * @return 初始化好path的ShapeLayer */
- (CAShapeLayer *) p_handLayerWithWidth:(CGFloat)width height:(CGFloat)height tailLength:(CGFloat)tailLength tickLength:(CGFloat)tickLength; /** * 不含时钟运行标识判断与修改的私有方法,动画执行与UI更新主方法 * * @param time 要设置的时间戳 */
- (void) p_setTime:(NSTimeInterval)time; /** * 定时器的触发处理,更新钟表时间 */
- (void) p_handleTimeSource; @end

@implementation HSClockView { CAShapeLayer *_hourLayer; CAShapeLayer *_minuteLayer; CAShapeLayer *_secondLayer; NSTimer *_timer; }

8、结语

  写这个工程Demo差很少用了5个小时,编码速度还有待提升;在编码思路上,再思考是搭建现实世界桥梁,仍是直接转换成机器思惟,或者是将二者良好的综合运用。

  另外,要真正作好三维特效的动画,要对光源、材质,光线跟踪等方面有些了解,好比聚光灯、泛光灯、平行光;金属材质、塑料材质、玻璃材质;阴影反射变化等。笔者之前作过3DMax建模与动画,欢迎童鞋一块儿讨论。

  本文的工程源码:https://github.com/1962449521/OCDemos/tree/master/ClockDemo 

相关文章
相关标签/搜索