仿寫及比較標哥的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 

相關文章
相關標籤/搜索