iOS CALayer组合动画实现loadingView

前言

hihi,勇敢的小伙伴儿们,大家好,loadingView在生活中已经是很常见了,各大平台都有不同的loadingView,最常见的浏览器就能看到的如下图这种Google Chrome的等待动画,

还有很多种其他的loading动画需要我们学习,

iOS CALayer组合动画实现loadingView_第1张图片

有时候我们使用做好的gif就可以实现,那么我们为什么要学习动画的绘制呢?

因为CALayer动画可以实现比UIView动画更丰富、更底层、效率更高的动画。能用代码实现的尽量不用资源文件,实际上也是对我们应用的一种瘦身。

话不多说,送上我的Demo地址:CALayerDemo1 (名字起得很随意hhhh),然后开始我们今天的学习吧~

正文

这篇文章通过两个加载动画向大家介绍CALayer的动画。按照面向对象的思想说,Layer其实就是一个模型类,它包含若干属性,并没有任何处理逻辑的方法,这些属性影响着显示在Layer中的内容。我们先来看看UIView和CALayer之间有什么区别和联系。

  • 联系:Layer是View背后的那个女人。每一个UIView后面都有对应的CALayer,大家看到的在UIView中显示的内容其实是在CALayer中。
  • 区别:
    • View有复杂的、各种组合的布局机制。Layer只有极简单的布局。
    • View可以响应用户交互。Layer不能响应用户交互。
    • View中的绘画逻辑有CPU执行。Layer中的绘画直接有GPU执行。
    • View有丰富的、功能强大的子类。Layer只有很少的几个子类。
    • View动画属性较少,局限性较大。Layer由于更底层、动画属性更多,所以可以实现出更灵活、更丰富的动画。

第一个CALayer动画

Layer动画系列的文章,我不准备系统的从简单到复杂的知识进行讲解,我会通过各种实战示例,示例中用到什么知识点就讲什么知识点。

第一个动画让我们来实现Google Chrome浏览器加载时页签上的等待动画~

新建项目CALayerDemo1,打开Main.storyboard,拖拽一个UIView到ViewController中,添加好约束,自行设置ViewController和UIView的背景色,这里UIView的背景色我设置为无色:

iOS CALayer组合动画实现loadingView_第2张图片

然后添加该View的Outlet到ViewController中,这个UIView就是要展示加载动画的View:

@property (weak, nonatomic) IBOutlet UIView *loadingView;

打开ViewController.m,申明一个常量属性ovalShapleLayer

CAShapeLayer *ovalShapeLayer = [CAShapeLayer layer];

ovalShapleLayer的类型是CAShapleLayer,它是CALayer的为数不多的子类之一。它的作用是在屏幕上画出各种形状,不论是简单的圆形、方形还是复杂的五角星或不规则图形都难不住它。CAShapeLayer有如下一些主要属性:

  • strokeColor:笔画颜色。
  • strokeStart:笔画开始位置。
  • strokeEnd:笔画结束位置。
  • fillColor:图形填充颜色。
  • lineWidth:笔画宽度,即笔画的粗细程度。
  • lineDashPattern:虚线模式。
  • path:图形的路径。
  • lineCap:笔画未闭合位置的形状。

我们之所要申明一个CAShapeLayer,是因为要用它在屏幕上画出一个圆形。下面在viewDidLoad()方法中添加如下代码:

    ovalShapeLayer.strokeColor = [UIColor whiteColor].CGColor;
    ovalShapeLayer.fillColor = [UIColor clearColor].CGColor;
    ovalShapeLayer.lineWidth = 7;

这几个属性刚才已经向大家介绍过了,这三行代码的意思是我们画出的圆形笔画颜色是白色,没有填充色,笔画的宽度为7。接着我们申明这个圆形的半径,使这个圆形的大小为容纳它视图大小的80%:

    CGFloat ovalRadius = _loadingView.frame.size.width / 2. * 0.8;

最后我们设置ovalShapeLayer的路径,这是最关键的一步,因为你要告知CAShapeLayer按照什么路径绘制图形,让我们接着添加如下代码:

    CGPathRef path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_loadingView.frame.size.width/2 - ovalRadius , _loadingView.frame.size.height/2 - ovalRadius,  ovalRadius * 2,  ovalRadius * 2)].CGPath;
    ovalShapeLayer.path = path;

这里出现了新面孔UIBezierPath,它可以创建基于矢量的路径,是Core Graphics框架关于path的封装。UIBezierPath可以定义简单的形状路径,如椭圆、矩形,或者有多个直线和曲线段组成的形状。在这里我们要使用它的初始化方法init(ovalInRect rect: CGRect)定义一个正圆的路径。设置完路径后,将ovalShapeLayer添加到loadingView视图的Layer中,它就可以按照设定好的路径在loadingView中绘制图形了:

[_loadingView.layer addSublayer:ovalShapeLayer];

编译运行看看效果:

iOS CALayer组合动画实现loadingView_第3张图片

完美的一个圆形。接下来我们要做的是让这个圆只显示一部分,因为Google的加载动画只有大概五分之二的圆形轮廓。让我们继续将目光集中在viewDidLoad()方法中,在[_loadingView.layer addSublayer:ovalShapeLayer]这行代码上面添加另一行代码:

ovalShapeLayer.strokeEnd = 0.4;

上面的代码将ovalShapeLayerstrokeEnd属性设置为0.4,意思是ovalShapeLayer在绘制圆形时只画整个圆形的五分之二,即笔画结束的位置在整个圆形轮廓的五分之二处。编译运行看看效果:

iOS CALayer组合动画实现loadingView_第4张图片

看来是我们想要的效果,但是仍有一处细节需要我们完善,看看Google的加载动画,蓝色的部分圆形轮廓两头是圆形的,而我们的圆形轮廓两头是方形的。这个问题很好解决,仍然在[_loadingView.layer addSublayer:ovalShapeLayer]这行代码上面添加一行代码:

ovalShapeLayer.lineCap = kCALineCapRound;

这行代码的意思是将笔画两头的形状设置为圆形,对应的还有两个常量kCALineCapButt ,kCALineCapSquare,大家可以试试。再次编译运行看看效果:

iOS CALayer组合动画实现loadingView_第5张图片

到目前为止,我们通过CALayer绘制出了动画的主体,接下来要让它动起来。在ViewController.m中添加beginSimpleAnimate()方法:

- (void)beginSimpleAnimation {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    animation.duration = 1.5;
    animation.fromValue = 0;
    animation.toValue = @(M_PI * 2);
    animation.repeatCount = HUGE;
    [_loadingView.layer addAnimation:animation forKey:nil];
}

在这个方法中,我们又看到了新面孔CABasicAnimation,该类提供了基本的、单关键帧的Layer属性动画,通过animationWithKeyPath:初始化方法,根据keyPath创建不同的CAPropertyAnimation实例。常用的keyPath有如下一些:

  • transform.rotation:旋转动画。
  • transform.ratation.x:按x轴旋转动画。
  • transform.ratation.y:按y轴旋转动画。
  • transform.ratation.z:按z轴旋转动画。
  • transform.scale:按比例放大缩小动画。
  • transform.scale.x:在x轴按比例放大缩小动画。
  • transform.scale.y:在y轴按比例放大缩小动画。
  • transform.scale.z:在z轴按比例放大缩小动画。
  • position:移动位置动画。
  • opacity:透明度动画。

以上只是一部分常用的动画keyPath,更多的希望大家在实际运用中去挖掘。在beginSimpleAnimation()方法中,我们使用了transform.rotation,创建了一个旋转动画的实例,然后给该动画设置了四个属性:

  • duration:动画持续时间。
  • fromValue:动画起始值。
  • toValue:动画结束值。
  • repeatCount:重复次数。

该方法设置这几个属性的含义为使动画主体不停的旋转,旋转一圈的时间为1.5秒。以上这几个概念在UIView的动画中同样存在,大家应该都已经比较熟悉了。然后使用Layer的addAnimation方法将旋转动画实例添加到目标Layer中,该方法的key是用来标示添加的动画,便于以后重复使用时能方便的检索,如果没有需求可以传值nil。最后viewWillAppear方法中调用beginSimpleAnimation()方法:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self beginSimpleAnimation];
}

编译运行看看效果:

iOS CALayer组合动画实现loadingView_第6张图片

至此我们的第一个简单的CALayer动画就完成了,在下一节我们一起实现一个更加有意思的加载动画,从而向大家介绍新的动画类型及动画组合。

Stroke Animation与Animation Group

让我们先看看要实现的效果:

iOS CALayer组合动画实现loadingView_第7张图片

这种加载动画在很多应用中都出现过,比如网易新闻、Win版的谷歌浏览器中都有使用。下面就让我们一步一步来实现吧,首先打开Main.storyboard,新添加一个UIView,在ViewController.m中添加Outlet:

iOS CALayer组合动画实现loadingView_第8张图片

@property (weak, nonatomic) IBOutlet UIView *animationView;

然后定义一个新的CAShapeLayer:

 CAShapeLayer *aniShapeLayer = [CAShapeLayer layer];

viewDidLoad()方法中对它进行设置,并将其添加到刚才创建的animationView中:

    aniShapeLayer.strokeColor = [UIColor whiteColor].CGColor;
    aniShapeLayer.fillColor = [UIColor clearColor].CGColor;
    aniShapeLayer.lineWidth = 7;
    CGFloat aniRadius = _animationView.frame.size.width / 2. * 0.8;
    CGPathRef aniPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_animationView.frame.size.width/2 - aniRadius , _animationView.frame.size.height/2 - aniRadius,  aniRadius * 2,  aniRadius * 2)].CGPath;
    aniShapeLayer.path = aniPath;
    aniShapeLayer.lineCap = kCALineCapRound;
    [_animationView.layer addSublayer:aniShapeLayer];

这些操作在上一个动画都已经做过一遍了,这里就不再解释。编译运行看看是否屏幕上又出现了一个圆圈呢:

iOS CALayer组合动画实现loadingView_第9张图片

接下来在ViewController.m中添加一个方法beginComplexAnimation()

- (void)beginComplexAnimation {
    /*
     strokeStartAnimate动画让绘制圆圈的笔画起始位置从–0.5开始,目的是让笔画起始绘制时等待一段时间,也就是起始位置延迟绘制。而strokeEndAnimate动画让绘制圆圈的笔画终止位置正常的从0绘制到1。这样一来笔画两头绘制的时间就会不一样,会有一个时间差,这样就有圆圈不断绘制又不断被擦除的效果。
     */
    //笔画开始动画
    CABasicAnimation *strokeStartAnimate = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    //画到0.5的时候笔画结束的动画执行形成在半圆处出现擦除效果0.25则为在1/4圆处出现擦除效果
    strokeStartAnimate.fromValue = @(-0.5);
    strokeStartAnimate.toValue = @(1);
    
    //笔画结束动画
    CABasicAnimation *strokeEndAnimate = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    //0代表绘制路径的起始位置
    strokeEndAnimate.fromValue = @(0);
    //1代表绘制路径的终止位置
    strokeEndAnimate.toValue = @(1);
    
    CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
    group.duration = 1.5;
    group.repeatCount = HUGE;
    group.animations = @[strokeStartAnimate,strokeEndAnimate];
    //用_animationView.layer 添加动画 不可行 需要加在layer上
    [aniShapeLayer addAnimation:group forKey:nil];
}

这里出现了两个新的动画类型,笔画开始动画和笔画结束动画,我们虽然使用CAShapeLayer绘制了一个圆圈,但是它也存在笔画起始位置和笔画终止位置,只不过它俩在同一个位置而已,笔画动画的位置取值在0–1之间,0代表绘制路径的起始位置,1代表绘制路径的终止位置。

所以strokeStartAnimate动画让绘制圆圈的笔画起始位置从–0.5开始,目的是让笔画起始绘制时等待一段时间,也就是起始位置延迟绘制。而strokeEndAnimate动画让绘制圆圈的笔画终止位置正常的从0绘制到1。这样一来笔画两头绘制的时间就会不一样,会有一个时间差,这样就有圆圈不断绘制又不断被擦除的效果。

strokeStartAnimatestrokeEndAnimate是两个动画,如何作用于一个Layer上呢?这时就要用到CAAnimationGroup,顾名思义它是将多个动画组成一个组,在一个动画组里,子动画会同时进行。动画组可以设置动画持续时间、重复次数以及子动画数组。最后将动画组加在Layer上即可。

最后在viewWillAppear()方法中调用beginComplexAnimation()方法:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self beginSimpleAnimation];
    [self beginComplexAnimation];
}

编译运行看看效果:

iOS CALayer组合动画实现loadingView_第10张图片

总结

CALayer动画可以实现比UIView动画更丰富、更底层、效率更高的动画。但是在实际的应用开发中,我们应该按需所用,能用UIView动画实现的我们就可以不用CALayer动画,它俩没有谁优谁劣之分。这篇文章只是CALayer动画的引子,让大家对CALayer动画有初步的了解和认识,之后我在文章中会通过更多的实例帮大家更深入的认识CALayer动画,从而提升自己应用的用户体验。

感谢大家看到最后,这篇文章是我按照付宇轩(@DevTalking)的博文里一步一步学习,改成OC的代码并添加自己的学习笔记做了自己的Demo完成的,在这里非常感谢前辈的分享,Swift的原文地址:CALayer Animation - Loading Indicator,如果有问题,还请大家留言给我,感激不尽~

另外,第二个loading图的实现也希望大家能去动脑想一下~

经过半小时的努力,emmm,我的实现效果如下,代码在demo里更新了~

iOS CALayer组合动画实现loadingView_第11张图片

你可能感兴趣的:(iOS)