用QuartzCode快速实现一个收藏动画

前言

“工欲善其事,必先利其器”,iOS的Core Animation非常强大,可以做出非常炫酷的动画,但是实际写起来还是比较麻烦,代码不直观,需要反复的改参数、Build、运行,所以诞生了各种动画制作工具,本文就以其中比较有名的QuartzCode为例,快速实现一个收藏功能的星星动画,最终的效果如下:

GitHub地址:https://github.com/zekunyan/QuartzCodeExample-StarAimation

QuartzCode简介

QuartzCode是一款专门用来创作iOS/macOS动画的软件,基本上把整个Core Animation框架的内容都搬到了软件里面,比如各种Layer、Mask、可以用来动画的属性,基本上跟代码是一致的,完全可以当成“可视化动画编程”工具,而且制作以后可以导出代码+资源文件,直接就可以在代码里面调用,方便自定义修改。

以QuartzCode 1.54.0为例,主体界面如下:

简单讲下:

  • A: Layers,用于动画的Layer在这里显示,先后顺序、层级和StoryBoard比较像
  • B: Inspector,当前选中元素,如Layer的属性编辑,基本和对应CALayer类的属性一一对应
  • C: Animations,动画组,可以有多个动画组,每个相互独立
  • D: Timeline,动画时间轴,每个Layer的动画都会在时间轴上标出
  • E: Keyframe inspector,动画关键帧的属性编辑,也是跟Core Animation的CAAnimation类以及子类的属性一致
  • F: 画布,Layer的编辑区

总体上来看,QuartzCode的界面跟XIB、StoryBoard的风格非常类似,还是很容易上手的,更多的教程看官网:http://www.quartzcodeapp.com/

整理思路

首先整理下思路:

总体上可以分成两个部分,一个是动画部分,用QuartzCode实现可视化编辑创建;另一个是功能、代码API设置部分,需要在QuartzCode最后导出的代码基础上,添加、修改,封装成简单的控件库。

QuartzCode实现动画

先来看看动画的实现,本节只把主要步骤流程列出来,详细的参数、设置,参考GitHub上的QuartzCode文件

显示星星动画

显示星星的动画由三个部分组成,分别是实心的星星从小到大出现,空心的从大到小并透明度减小消失,最后星星四周闪动圆点、短线。

实心星星从小到大出现

  1. 拖入图片素材到中间的画布上,软件会自动创建一个layer,可以通过Inspector调整大小、位置
  2. 对Layers里面自动生成的layer重命名
  3. 点击Animations的“+”按钮,添加一个动画组,命名为show
  4. 选中当前的layer,点击Timeline下对应layer的“+”按钮,添加一个Bounds帧动画
  5. Timeline里面的每一行对应帧动画的时间轴,可以在轴上双击、右键添加关键帧,这里默认就好,分别选中Bounds帧动画时间轴两端的关键点,在右边的Keyframe inspector里面设置Size值,起始是(0,0),结束时大小为原图大小

空心星星从大到小、透明度减小消失

基本和上一步一样,可以先隐藏实心的layer以便于观察。

  1. 添加空心的星星图片素材
  2. 命名layer
  3. 给空心的layer添加Bounds和Opacity两种动画,分别修改帧动画的起始、结束值
  4. 拖动时间轴查看效果

四周闪动短线、圆点

短线、圆点是沿着星星的四周重复排列的,所以这里可以用CAReplicatorLayer来实现,对应到QuartzCode里面就是工具栏的”Replicator”类型的Layer。CAReplicatorLayer可以根据一定的规则重复的绘制内部的子Layer,在这里只需要按照旋转角度变化就好。

  1. 点击”Replicator”,在中间的画布上拖动出一个layer,然后选择”Shape”里面的圆角矩形,在星星的一角拖动画出一个小的圆角矩形,保证star_layer在replicator layer的内部
  2. 设置replicator的属性,实现循环重复内部的layer,这里设置”instance count”为5,”Rotation”为72,也就是360/5=72,就可以实现按照星星的五个角重复绘制
  3. 最后就是添加动画,分别是Opacity透明度从0到1,Position从内到外,Transform变化大小

剩余的圆点,和短线的创建方法一致,就不再重复讲解~

至此,显示的动画就创建好了。

隐藏星星动画

再来看看隐藏星星的动画,有了上一小节的基础,隐藏星星的动画就容易多了,基本步骤都是一样的,如下:

  1. 添加一个新的动画组hide
  2. 分别设置每个layer的动画,实心的星星从大到小、同时透明度降低,空心的相反,replicator layer一直隐藏

导出动画代码

动画都实现了好了以后,就可以导出代码了。

依次File->Export Code...,就可以打开导出代码的预览界面:

右边的选项,按照需要选择,默认的基本可以满足要求,“Export Image Assets”最好选上,可以一同导出图片资源。选项的说明参见文档:http://www.quartzcodeapp.com/code-generation/code-options/

至此,最复杂的动画部分就完成了,不用写一行代码,全部可视化编辑~

调整、封装代码

接着就是封装导出的代码,方便使用。

API设计

首先是整个动画的API的设计,为了方便给任意UIView实例添加动画,所以设计成UIView的Category,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//  UIView+TTGStarAnimation.h
@interface UIView (TTGStarAnimation)

// 初始化动画
- (void)ttg_setupStarAnimation;

// 初始化动画,同时可以调整inset
- (void)ttg_setupStarAnimationWithEdgeInset:(UIEdgeInsets)inset;

// 显示星星动画
- (void)ttg_showStarAnimated:(BOOL)animated complete:(void (^)())complete;

// 隐藏星星动画
- (void)ttg_hideStarAnimated:(BOOL)animated complete:(void (^)())complete;

// 当前是否显示
- (BOOL)ttg_getStarIsShow;

@end

AssociatedObject保存动画View实例

QuartzCode导出的动画代码都在一个View上,所以为了在Category里给任意UIView实例添加动画View的Property、当前星星是否显示的Bool值,就要用到AssociatedObject,对应的set、get方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//  UIView+TTGStarAnimation.m

- (TTGStarAnimationView *)ttg_getStarAnimationView {
return objc_getAssociatedObject(self, &kTTGButtonStarAnimationViewKey);
}

- (BOOL)ttg_getStarIsShow {
return [objc_getAssociatedObject(self, &kTTGButtonStarAnimationStateKey) boolValue];
}

- (void)ttg_setStarIsShow:(BOOL)isShow {
objc_setAssociatedObject(self, &kTTGButtonStarAnimationStateKey, @(isShow), OBJC_ASSOCIATION_COPY_NONATOMIC);
}

修改QuartzCode导出的代码 - 暴露内部Layer实例

QuartzCode导出的代码,内部的动画Layer是不可见的,为了能“手动”控制动画View的实际显示状态,就要修改下导出的代码,暴露内部的动画Layer实例。

所有的Layer都在NSMutableDictionary * layers字典里面,通过key获取,给TTGStarAnimationView.h添加三个属性:

1
2
3
4
5
6
7
//  TTGStarAnimationView.h
@interface TTGStarAnimationView : UIView
@property (nonatomic, strong, readonly) CALayer *fillLayer;
@property (nonatomic, strong, readonly) CALayer *emptyLayer;
@property (nonatomic, strong, readonly) CALayer *replicatedLayer;
// ...
@end

然后实现getter方法:

1
2
3
4
5
6
7
8
9
10
11
12
//  TTGStarAnimationView.m
- (CALayer *)fillLayer {
return _layers[@"star_animation_fill"];
}

- (CALayer *)emptyLayer {
return _layers[@"star_animation_empty"];
}

- (CALayer *)replicatedLayer {
return _layers[@"replicator"];
}

初始化

ttg_setupStarAnimationttg_setupStarAnimationWithEdgeInset:方法,用来初始化动画,包括创建TTGStarAnimationView实例,添加、保存在当前view上,设置每个动画layer的状态等操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//  TTGStarAnimationView.m

- (void)ttg_setupStarAnimation {
[self ttg_setupStarAnimationWithEdgeInset:UIEdgeInsetsZero];
}

- (void)ttg_setupStarAnimationWithEdgeInset:(UIEdgeInsets)inset {
// 获取当前实例的动画starAnimationView
TTGStarAnimationView *starAnimationView = [self ttg_getStarAnimationView];

if (!starAnimationView) {
// 不存在,创建新的
starAnimationView = [[TTGStarAnimationView alloc] initWithFrame:UIEdgeInsetsInsetRect(self.bounds, inset)];
// 设置不可接受交互事件,防止阻断父view
starAnimationView.userInteractionEnabled = NO;
// 设置AutoSizingMask
starAnimationView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
// 添加
[self addSubview:starAnimationView];
// AssociatedObject保存实例
objc_setAssociatedObject(self, &kTTGButtonStarAnimationViewKey, starAnimationView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// 移除所有动画
[starAnimationView removeAllAnimations];

// 设置layer的初始化状态
starAnimationView.fillLayer.hidden = YES;
starAnimationView.emptyLayer.hidden = NO;
starAnimationView.replicatedLayer.hidden = YES;

// 设置初始化状态值
[self ttg_setStarIsShow:NO];
}

动画控制

最后就是实现动画的控制,这里比较关键的是对动画的每个Layer的控制,每个Layer的初始、最终状态都要设置,保证多次调用、无论是否执行动画,最终显示都是一致的,以显示星星为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//  TTGStarAnimationView.m

- (void)ttg_showStarAnimated:(BOOL)animated complete:(void (^)())complete {
// 获取当前view的动画starAnimationView实例
TTGStarAnimationView *starAnimationView = [self ttg_getStarAnimationView];

// 当前是hide,才执行show
if (![self ttg_getStarIsShow]) {
// 设置状态值
[self ttg_setStarIsShow:YES];

// 先移除所有动画
[starAnimationView removeAllAnimations];

// 设置最终状态下Layer的hidden值的Block
void (^completionBlock)() = ^ {
starAnimationView.fillLayer.hidden = NO;
starAnimationView.emptyLayer.hidden = YES;
starAnimationView.replicatedLayer.hidden = YES;

// 执行回调
if (complete) {
complete();
}
};

if (animated) {
// 如果要执行动画,则设置动画前Layer的hidden值
starAnimationView.fillLayer.hidden = NO;
starAnimationView.emptyLayer.hidden = NO;
starAnimationView.replicatedLayer.hidden = NO;
// 执行动画
[starAnimationView addShowAnimationCompletionBlock:^(BOOL finished) {
completionBlock();
}];
} else {
// 不用动画,直接执行最终状态
completionBlock();
}
}
}

隐藏的代码跟显示基本一致,把各种状态、hidden的值倒过来就可以了。
所有代码参见Github

要写的代码就这么多,是不是非常简单!

总结

“工欲善其事,必先利其器”,借助各种强大的工具软件,做动画的效率简直提升几十倍~

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器