SCCatWaitingHUD的Objc實現分享

原文發表在 Animatious一塊兒動畫開源組git

歡迎你們在微博github關注咱們github

介紹

這個創意其實來源於微博上@畫渣程序猿mmoaay轉發並艾特個人的一組gif圖
看到圖的時候 我先對圖大體進行告終構和層次的區分
在設計物體動效的時候,首先是要對動畫的不一樣對象進行拆分,在這裏,老鼠,貓眼睛,眼皮,以及貓臉,他們各自的行爲分別是不一樣的,所以分爲不一樣的圖層 windows

在總體動畫進行的時候,他們各自的layer所執行的動畫是不一樣的
看起來這個控件很簡單,接下來咱們就來分析一下寫出這樣一個控件須要注意的關鍵點吧~app

分析

先確認咱們要作成什麼樣的控件ide

首先 這是一個等待動畫 因此應該作成一個HUD類型的控件

這樣能夠在當前window的上方出現和消失而不影響當前視圖的展現
因此咱們就要在應用當前默認的UIWindow上再添加一個UIWindow 這樣能夠在全局任何地方調用這個HUD 同時咱們的HUD也要實現全局的單例模式函數

其次是動畫 最初的動畫其實並不難

只是眼睛圖層和老鼠圖層圍着各自的中心點旋轉 且擁有相同的運動週期 這樣能讓他們在相同時間內移動的弧度是相同的 產生眼睛跟着老鼠走的感受動畫

最後是眼皮和眼珠的運動協調

因爲眼珠作的是勻速圓周運動 而眼皮作的是直線縮放運動 所以如何協調這二者的運動時間曲線 決定了最後的動畫是否流暢和天然ui

還有一點額外的臨時產生的需求就是對橫屏的支持

這是我在寫的過程當中@sauchye提出的issue,因此我花了一個版本用來加上了對左右橫屏的支持this

代碼實現

UIWindow

UIWindow 是每個程序都必須建立的根視圖,它是UIView的子類,可是地位卻在全部的UIView之上。每個程序在啓動的時候,若是你使用了interface builder做爲啓動界面,那麼Xcode會自動把你的第一個Controller添加到根window上,若是你要經過代碼初始化Window,則必需要像系統要求的這樣聲明:spa

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

   UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

   myViewController = [[MyViewController alloc] init];
   window.rootViewController = myViewController;
   [window makeKeyAndVisible];

   return YES;

}

在SCCatWaitingHUD中,因爲須要存在一個不干擾原有程序代碼邏輯和視圖邏輯的View,所以最合適的辦法就是聲明一個獨立的Window。HUD的這種使用模式能夠參考MBProgressHUD,開發者能夠在全局任意的地方調用HUD的顯示,因爲HUD所在的Window和系統本來的Window相比level更高,因此能夠在不影響原有視圖的狀況下在任意頁面顯示。

UIWindow的結構以下,咱們能夠看到UIWindow自己就是一個UIView。每一個UIWindow都會有一個名爲rootViewController的屬性,而這個屬性就是這個window將會呈現的controller對象。

Xcode7以後,蘋果要求全部的UIWindow在聲明的時候都須要有一個rootViewController,即經過代碼聲明的時候,須要定義一個rootViewController,而後在這個controller之上添加要顯示的內容。可是通過驗證,在程序運行中建立的非根Window的UIWindow,能夠直接當作UIView來使用,仍然不須要強制給一個rootViewController。

self.backgroundWindow = [[UIWindow alloc]initWithFrame:self.frame];
_backgroundWindow.windowLevel = UIWindowLevelStatusBar;
_backgroundWindow.backgroundColor = [UIColor clearColor];
_backgroundWindow.alpha = 0.0f;

因此咱們這個HUD的主要顯示對象就是名爲backgroundWindow的UIWindow屬性對象
那麼怎麼控制Window的出現和消失呢
UIWindow有幾個方法:

- (void)makeKeyWindow;
- (void)makeKeyAndVisible;                             // convenience. most apps call this to show the main window and also make it key. otherwise use view hidden property

通常都是用上述第二個方法來讓指定Window成爲keyWindow而且出現。至於怎麼讓指定Window消失,蘋果並無提供一些特別的辦法,官方文檔中有給出下面這種用法

_backgroundWindow.hidden = YES;
// According to Apple Doc : This is a convenience method to make the receiver the main window and displays it in front of other windows at the same window level or lower. You can also hide and reveal a window using the inherited hidden property of UIView.

Animation

動畫要解決的第一個問題就是,老鼠的旋轉是繞着自身的中心點,然而兩個眼珠的旋轉並非
首先,旋轉動畫的實現咱們確定是經過CATransform3DRotate,基本的CAAnimation聲明以下:

double radians(float degrees) {
    return ( degrees * 3.14159265 ) / 180.0;
}
CATransform3D getTransForm3DWithAngle(CGFloat angle)
{
    CATransform3D  transform = CATransform3DIdentity;
    transform  = CATransform3DRotate(transform, angle, 0, 0, 1);
    return transform;
}
- (CABasicAnimation *)rotationAnimation
{
    CABasicAnimation *rotationAnimation;
    rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
    rotationAnimation.fromValue = [NSValue valueWithCATransform3D:getTransForm3DWithAngle(0.0f)];
    rotationAnimation.toValue = [NSValue valueWithCATransform3D:getTransForm3DWithAngle(radians(180.0f))];
    rotationAnimation.duration = self.animationDuration;
    rotationAnimation.cumulative = YES;
    rotationAnimation.repeatCount = HUGE_VALF;
    rotationAnimation.removedOnCompletion=NO;
    rotationAnimation.fillMode=kCAFillModeForwards;
    rotationAnimation.autoreverses = NO;
    rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    return rotationAnimation;
}

咱們須要來看看CALayer的幾個基本屬性:frame、bounds、position、anchorPoint。frame和bounds均可以用UIView中的知識去理解,frame是相對於superLayer的位置和大小,而bounds是origin爲(0,0)的frame。
而anchorPoint則是一個Layer的錨點,相信作過一些動畫開發或者遊戲開發的同窗們都知道錨點的做用。錨點做爲Layer變換的基準點,它的座標值爲相對於bounds寬高的比例。在iOS中,它的起始點是Layer的左上角,爲標準原點(0,0),右下角爲(1,1),默認值爲中心點(0.5,0.5);在MacOS中,起始點爲左下角,(1,1)在右上角。因此經過修改錨點的相對位置,就可讓旋轉的軸心變成咱們須要的位置。

這裏有一張圖來講明錨點是什麼。

所以我嘗試着將眼珠的anchorPoint設置爲(1.5,1.5),旋轉中心變成了接近眼睛中心的點。可是這時候咱們還須要修改position來配合anchorPoint的改變。咱們能夠這麼理解,position是錨點相對於父layer的座標值,而anchorPoint則是錨點相對於自身的座標值,二者共同決定了錨點和整個Layer所在的位置。
因此咱們還須要調整眼珠Layer的position,來和以前設置的anchorPoint的位置重合。

_rightEye.layer.anchorPoint = CGPointMake(1.5f, 1.5f);
_rightEye.layer.position = CGPointMake(self.faceView.right - 13.5f, self.faceView.top + width/3.0f + 7.5f);

這時候,上面的CAAnimation就能夠正常的添加到Layer的transform上而且執行出來啦。

眼皮的運動時間曲線

這裏最重要的一點就是要讓眼皮和眼珠的運動達到協調一致,不會出現翻白眼也不會出現鬥雞眼(哈哈哈哈)。因此咱們能夠先分析出下面兩個結論:

  • 眼皮的上下運動週期和眼珠的旋轉運動週期一致

  • 眼皮在豎直方向上的位移和眼珠在豎直方向上的位移一致

  • 眼珠在圓周切線方向的線性速率是勻速的

所以咱們能夠得出結論,眼皮在豎直方向上的速率曲線和眼珠在豎直方向上的速率曲線是一致的。咱們來分析眼珠的速度方向。

經過將速度分解成水平X軸和豎直Y軸的份量後,咱們能夠得出豎直方向上的眼珠速度的速度曲線,就是簡單的

函數圖像以下

這裏的y就是豎直速度份量Vy,x是t時刻的圓心角Q,因爲這裏作的是勻速圓周運動,所以角速度也是勻速的。咱們能夠將半周的時長定爲常量T,半周的弧度是π,所以x和t的關係能夠表示爲

你們知道位移等於速率乘以時間,所以咱們有了速率公式,就能夠得出眼皮在豎直方向上的位移公式以下

這裏我將常量T的值直接帶入了。

這個函數的圖像以下

注意,咱們得出這個函數圖像的主要目標是肯定眼皮的上下縮放動畫的時間曲線,這個曲線表明着位移和時間的函數關係。這時候,這個函數圖像就是接下來要給眼皮加上的縮放動畫的時間曲線了,我沒有用專門的畫貝塞爾曲線的軟件來畫,只是根據函數的圖像大體畫出了一個貝塞爾曲線,做爲了下面這個動畫的時間曲線

- (CAAnimationGroup *)scaleAnimation
{
    // 眼皮和眼珠須要肯定一個運動時間曲線
    CABasicAnimation *scaleAnimation;
    scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
    scaleAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
    scaleAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 3.0, 1.0)];
    scaleAnimation.duration = self.animationDuration;
    scaleAnimation.cumulative = YES;
    scaleAnimation.repeatCount = 1;
    scaleAnimation.removedOnCompletion= NO;
    scaleAnimation.fillMode=kCAFillModeForwards;
    scaleAnimation.autoreverses = NO;
    scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.2:0.0 :0.8 :1.0];
    scaleAnimation.speed = 1.0f;
    scaleAnimation.beginTime = 0.0f;
    
    CAAnimationGroup *group = [CAAnimationGroup animation];
    group.duration = self.animationDuration;
    group.repeatCount = HUGE_VALF;
    group.removedOnCompletion= NO;
    group.fillMode=kCAFillModeForwards;
    group.autoreverses = YES;
    group.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.2:0.0:0.8 :1.0];
    
    group.animations = [NSArray arrayWithObjects:scaleAnimation, nil];
    return group;
}

你能夠看到CAMediaTimingFunction,這就是時間曲線的類,在通常狀況下,系統會提供這麼幾種你經常使用到的時間曲線

CA_EXTERN NSString * const kCAMediaTimingFunctionLinear
    __OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseIn
    __OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseOut
    __OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseInEaseOut
    __OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);

他們分別表明了勻速運動,先快後慢,先慢後快,先快後慢再快這四種運動時間曲線。若是你要自定義時間曲線的話,系統也提供了繪製貝塞爾曲線的方法,你只要肯定曲線的兩個控制點座標便可。而座標的原點爲(0,0),最後收尾的點則在(1,1),橫軸爲時間,縱軸爲動畫的執行程度,座標值表明着相對比例值,所以咱們能夠截取上面的函數圖像中橫軸0到π的部分做爲0到1的時間長度,來繪製貝塞爾曲線。

最後是一點對橫屏的支持

因爲咱們實現的是一個獨立的UIWindow,沒有添加ViewController,因此不能經過ViewController下面幾個涉及到屏幕旋轉的回調來得到屏幕方向的變化。

// New Autorotation support.
- (BOOL)shouldAutorotate NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
- (UIInterfaceOrientationMask)supportedInterfaceOrientations NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
// Returns interface orientation masks.
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;

這時候咱們就要經過監聽系統的一個廣播消息來獲取屏幕狀態的變化。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(statusBarOrientationChange:)  name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];

在橫屏切換的回調裏,我記錄了前一個屏幕狀態,獲取到當前屏幕狀態,而後作了一個旋轉的變換,這樣整個HUD就會隨着屏幕方向的變化而變化了。詳細的實現能夠直接看源代碼,這裏就再也不貼出。

效果

這個開源控件的基本原理也就這些了,你能夠在個人github上clone下源代碼來閱讀 :-) ,若是你有更好的建議或者想法也歡迎隨時提PR。 這裏貼上一張效果圖。

其餘資源

文章內文件提供公共下載
配圖所用Sketch文件:下載連接

做者

Sergio Chan
Github : https://github.com/SergioChan
Weibo : http://weibo.com/sergiochan

相關文章
相關標籤/搜索