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

相关文章
相关标签/搜索