一、前言
以前看各種絢麗的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有強引用的問題,基本有四種解決方案:弱引用,關聯對象,中間代理,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時間戳到三個指針的弧度角行向量的轉換,代碼如下:
/** * 時針、分針、秒針的弧度角(z軸向外) */ 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軸向外。旋轉正方向二維時為逆時針,三維時看向旋轉軸的負方向,逆時針為旋轉角的增長方向。
3.繞任意軸旋轉時,先將坐標系轉換,使得旋轉軸與一坐標軸重合,在此坐標系完成旋轉後,再做坐標系逆轉換。
4.推導過程涉及到矩陣運算,相乘,求逆等;涉及到三角函數和差化積等。
七、工程中聲明的私有屬性、成員變量和私有方法
關於在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個小時,編碼速度還有待提高;在編碼思路上,再思考是搭建現實世界橋梁,還是直接轉換成機器思維,或者是將兩者良好的綜合運用。
本文的工程源碼:https://github.com/1962449521/OCDemos/tree/master/ClockDemo