zoukankan      html  css  js  c++  java
  • 仿写及比较标哥的iOS时钟动画

    一、前言

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

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

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

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

    二、仿写的UI动画结果比较

      原文链接:http://www.henishuo.com/clock-animation/

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

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

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

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

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

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

    三、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。

    四、类设计与编码

      在其它语言中,有接口的概念但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

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

      在虚拟时钟这个问题上还是比较简单的,主要在于时间字符串或者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);
    }

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

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

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

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

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

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

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

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

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

      关于在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;
    }

    八、结语

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

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

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

  • 相关阅读:
    bzoj 3876: [Ahoi2014&Jsoi2014]支线剧情【有上下界有源汇最小费用最大流】
    bzoj 2055: 80人环游世界【有上下界有源汇最小费用最大流】
    bzoj 2406: 矩阵【二分+有源汇上下界可行流】
    bzoj 4873: [Shoi2017]寿司餐厅【最大权闭合子图】
    bzoj 2007: [Noi2010]海拔【最小割+dijskstra】
    bzoj 2039: [2009国家集训队]employ人员雇佣【最小割】
    bzoj 3996: [TJOI2015]线性代数【最小割】
    bzoj 3158: 千钧一发【最小割】
    bzoj 2597: [Wc2007]剪刀石头布【最小费用最大流】
    bzoj 5120: [2017国家集训队测试]无限之环【最小费用最大流】
  • 原文地址:https://www.cnblogs.com/hushuai-ios/p/5295542.html
Copyright © 2011-2022 走看看