想必以前QQ空间的点赞效果大家都知道吧,点赞之后按钮周围会有一圈爆裂的小圆点;还有微信的红包雨表情动画等,以及烟花,火焰效果。这些看似很炫酷的动画可能让我们敬而远之,但是其实iOS封装的很好,利用简单的几行代码就能完成很炫酷的动画效果。由于目前正在玩儿iOS动画的内容,利用iOS的CAEmitterLayer结合CAEmitterCell能够达这些效果。不BB了,先上几个效果图。代码已传githubEmitterAnimation

红包雨

点赞效果

马丹 有没有办法一次性放很多个gif呀。。。。。。。


CAEmitterLayerCAEmitterCell

CAEmitterLayerCALayer的一个常用子类,CALayer的子类有很多,如果能很好的使用它们会得到一些意想不到的效果。CAEmitterLayer就是其中之一,CAEmitterLayer是用于实现基于Core Animation的粒子发生器系统。

`CALayer`的常用子类

所谓粒子,就是很多小颗粒,当让QQ空间的点赞动画的粒子也可以不用CAEmitterLayer粒子发生器来实现,不过这样会麻烦很多。在粒子系统中,CAEmitterLayer是用来发射粒子的,他所发射的粒子就是CAEmitterCell(当然粒子也可以发射粒子,也就是CAEmitterCell也可以发射CAEmitterCell)。可以认为CAEmitterLayerCAEmitterCell的工厂,通过不同的设置就会不断的产生想要的粒子。

原理其实很简单,但是动画就是这样,需要花时间去理解属性,只有很好的用它的属性,才能达到很炫酷的效果。查看API会发现CAEmitterLayerCAEmitterCell的属性都是很多的,并且有很多相同的属性。在CAEmitterLayer中,一些属性决定了粒子从什么样的几何特性上发射出来,这个几何特性包括了位置,形状,大小,并且还有一些渲染属性,用于一些渲染的效果。另外一些属性CAEmitterLayerCAEmitterCell都有的,在这里可能会迷糊,但是API说明的很清楚,CAEmitterLayer的这些属性会作为CAEmitterCell相同属性的系数,举个🌰,如果CAEmitterCellbirthRate = 10(每秒产生的粒子数量),其所属的CAEmitterLayerbirthRate = 2,那么在其他参数默认的情况下,这个CAEmitterCell总的每秒产生的粒子数量是10 * 2 = 20 。也就是每秒会产生20个这样的粒子。

另外,会发现CAEmitterCell的很多属性都带有一个Range,比如scaleRangevelocityRange,这些决定粒子自身的一些特性的属性大多都是以“中间值” + 范围(Range)的方式表示的。再举个🌰,比如scale = 0.5(缩放值)和scaleRange = 0.2(缩放的范围),那么表示的实际CAEmitterCell的缩放就是scale±scaleRange,即0.3~0.7这个范围。

红包雨demo

初步了解了这些之后,我们就可以跟着代码来实现实现一个红包雨的功能。实现起来很简单,只要设置好属性就行了,这些属性的详细含义会在下面的篇幅仔细讲解,先来试下一个小demo。就是最开始的第个效果图。

实现步骤
  • 设置CAEmitterLayer以及它的一些模式,并添加到要显示的view的图层上,当然也可以替换view的图层
  • 给这个CAEmitterLayer配置CAEmitterCell
  • 没有第三步了。
  1. 设置CAEmitterLayer以及它的一些模式,并添加到要显示的view的图层上,当然也可以替换view的图层。这里是直接添加到控制器的viewlayer上的,这个viewlayer是一个calyer类型的,如果想替换掉calyer,可以自定义view,并在view里面重写如下方法即可实现替换
// 替换view的layer
+ (Class)layerClass{return [CAEmitterLayer class];}

这里并没有替换,而是直接添加。当然为了后面方便这里把CAEmitterLayer设置为属性。详细代码如下,可以看注释

@interface ViewController ()
@property (nonatomic, strong) CAEmitterLayer * redpacketLayer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self redpacketRain];
    
}

/**
 * 红包雨
 */
- (void)redpacketRain{
    
    // 1. 设置CAEmitterLayer
    CAEmitterLayer * redpacketLayer = [CAEmitterLayer layer];
    [self.view.layer addSublayer:redpacketLayer];
    self.redpacketLayer = redpacketLayer;
    
    redpacketLayer.emitterShape = kCAEmitterLayerLine;  // 发射源的形状 是枚举类型
    redpacketLayer.emitterMode = kCAEmitterLayerSurface; // 发射模式 枚举类型
    redpacketLayer.emitterSize = self.view.frame.size; // 发射源的size 决定了发射源的大小
    redpacketLayer.emitterPosition = CGPointMake(self.view.bounds.size.width * 0.5, -10); // 发射源的位置
    redpacketLayer.birthRate = 0.f; // 每秒产生的粒子数量的系数
}

到这里CAEmitterLayer的就设置完了,需要注意的是发射源的emitterPosition这个属性,是将它放在了顶部,实现从天上掉下来的效果。

  1. 给这个CAEmitterLayer配置CAEmitterCell。在redpacketRain方法里面添加如下代码。值得注意的是,粒子的内容contents是一个CGImageRef类型的,用UIImage需要转换为CGImage.
    // 2. 配置cell
    CAEmitterCell * snowCell = [CAEmitterCell emitterCell];
    snowCell.contents = (id)[[UIImage imageNamed:@"red_paceket"] CGImage];  // 粒子的内容 是CGImageRef类型的
    
    snowCell.birthRate = 10.f;  // 每秒产生的粒子数量
    snowCell.lifetime = 20.f;  // 粒子的生命周期
    
    snowCell.velocity = 8.f;  // 粒子的速度
    snowCell.yAcceleration = 1000.f; // 粒子再y方向的加速的
    
    snowCell.scale = 0.5;  // 粒子的缩放比例
    
    redpacketLayer.emitterCells = @[snowCell];  // 粒子添加到CAEmitterLayer上

  1. 再添加一个按钮,通过用KVC设置CAEmitterLayerbirthRate来实现动画的开始和结束。
- (IBAction)redpacketClick:(id)sender {
    
    [self.redpacketLayer setValue:@1.f forKeyPath:@"birthRate"];
    
    [self performSelector:@selector(endRedpacketAnimation) withObject:nil afterDelay:2.f];
}

- (void)endRedpacketAnimation{
    [self.redpacketLayer setValue:@0.f forKeyPath:@"birthRate"];
}
  1. run之后点击按钮就会出现上图的效果了。

CAEmitterLayerCAEmitterCell的属性详解

实现上面的小demo是不是很简单?不到20行代码而已,用到的属性和也很少。那么下雨下雪的效果也是这样实现的,只不过是修改属性值。但是这些效果不够炫酷,要实现炫酷的效果得先了解各个属性的含义,那么接下来花大量的篇幅讲解CAEmitterLayerCAEmitterCell的属性。

CAEmitterLayer常用属性

@property(nullable, copy) NSArray<CAEmitterCell *> *emitterCells; // 用来装粒子的数组
@property float birthRate; // 粒子产生系数,默认1.0
@property float lifetime; // 粒子的生命周期系数, 默认1.0
@property CGPoint emitterPosition; // 决定了粒子发射形状的中心点
@property CGFloat emitterZPosition;
@property CGSize emitterSize; // 发射源的尺寸大小
@property CGFloat emitterDepth;
@property(copy) NSString *emitterShape; // 发射源的形状
@property(copy) NSString *emitterMode; // 发射模式
@property(copy) NSString *renderMode; // 渲染模式
@property BOOL preservesDepth;
@property float velocity; // 粒子速度系数, 默认1.0
@property float scale; // 粒子的缩放比例系数, 默认1.0
@property float spin; // 粒子的自旋转速度系数, 默认1.0
@property unsigned int seed; // 随机数发生器

CAEmitterLayer里面的API里面的所有属性都已经贴出来并作了说明,看看注释并调试一下就能理解大部分,接下来重点说说一些常用的属性

1. CAEmitterLayer控制粒子发射位置和形状的属性

CAEmitterLayer发射的粒子并不是杂乱无章的,我们可以设置它发射粒子时的位置、几何图形等。通过以下属性去配置:

  • emitterPosition:决定发射源的中心点,如果比如上面的demo中设置的为CGPointMake(self.view.bounds.size.width * 0.5, -10),那么x轴方向上就是在view的中心点,然后再设置emitterSize = CGSizeMake(40, 0);的话,那么就是self.view.bounds.size.width * 0.5的左右两边各20。API中还提供了一个emitterZPosition,这个是用在三维坐标中的,笔者暂时对三维的没有研究。
  • emitterSize: 决定发射源的大小
  • emitterShape:表示粒子从什么形状发射出来,它并不是表示粒子自己的形状。是一个枚举类型,提供如下类型可供选择:
kCAEmitterLayerPoint
kCAEmitterLayerLine
kCAEmitterLayerRectangle
kCAEmitterLayerCuboid
kCAEmitterLayerCircle
kCAEmitterLayerSphere
  1. kCAEmitterLayerPoint:点形状,发射源的形状就是一个点,位置在上面position设置的位置
  2. kCAEmitterLayerLine:线形状,发射源的形状是一条线,位置在rect的横向的位于垂直方向中间那条
  3. kCAEmitterLayerRectangle:矩形状,发射源的形状是一个矩形,就是上面生成的那个矩形rect
  4. kCAEmitterLayerCuboid:立体矩形形状(3D),发射源是一个立体矩形,这里要生效的话需要设置z方向的数据,如果不设置就同矩形状
  5. kCAEmitterLayerCircle:圆形形状,发射源是一个圆形,形状为矩形包裹的那个圆,二维的
  6. kCAEmitterLayerSphere:立体圆形(3D),三维的圆形,同样需要设置z方向数据,不设置则通二维一样

这些形状可以在调试的时候修改来看看有什么不同,比如我们设置的红包效果,就是用的kCAEmitterLayerLine,结合了emitterSize来是实现从顶部掉下来的效果,而emitterSizeheight其实是被忽略的。接下来我们看如下代码的效果图解:

redpacketLayer.emitterPosition = CGPointMake(100, 100);
redpacketLayer.emitterSize = CGSizeMake(20, 0);
redpacketLayer.emitterShape = kCAEmitterLayerLine;

达到的效果
emitterShape的几种模式其实很好理解,以emitterPosition的点为中心,然后作一个对应的形状,如直线、圆形、矩形,在这个形状上产生相应的粒子。

  • emitterMode:发射模式,这个字段规定了在特定形状上发射的具体形式是什么。它的作用其实就是进一步决定发射的区域是在发射形状的哪一部份。
kCAEmitterLayerPoints
kCAEmitterLayerOutline
kCAEmitterLayerSurface
kCAEmitterLayerVolume
  1. kCAEmitterLayerPoints:点模式,发射器是以点的形式发射粒子。发射点就是形状的某个特殊的点,比如shap是一个点的话,那么这个点就是中心点,如果是圆形,那么就是圆心。
  2. kCAEmitterLayerOutline:轮廓模式,从形状的边界上发射粒子。
  3. kCAEmitterLayerSurface:表面模式,从形状的表面上发射粒子。
  4. kCAEmitterLayerVolume:是相对于3D形状的“球体内”或“立方体内”发射,笔者暂时也不是很了解3D的。
2. CAEmitterLayer决定粒子系数的属性
  • birthRate: 粒子产生系数,默认1.0;每个粒子cell的产生率乘以这个粒子产生系数,得出每一秒产生这个粒子的个数。 即:每秒粒子产生个数 = layer.birthRate * cell.birthRate ;
  • lifetime:粒子的生命周期系数,默认1.0。计算方式同上;
  • velocity:粒子速度系数, 默认1.0。计算方式同上;
  • scale:粒子的缩放比例系数, 默认1.0。计算方式同上;
  • spin:自旋转速度系数, 默认1.0。计算方式同上;
3.CAEmitterLayer决定粒子内容的属性
  • emitterCells:用来装粒子的数组。每种粒子就是一个CAEmitterCell。在API中可以看到CAEmitterCell是服从CAMediatiming协议的,可以通过beginTime来控制subCell的出现时机。

CAEmitterCell常用属性

@property(nullable, copy) NSString *name; // 粒子名字, 默认为nil
@property(getter=isEnabled) BOOL enabled; 
@property float birthRate; // 粒子的产生率,默认0
@property float lifetime; // 粒子的生命周期,以秒为单位。默认0
@property float lifetimeRange; // 粒子的生命周期的范围,以秒为单位。默认0
@property CGFloat emissionLatitude;// 指定纬度,纬度角代表了在x-z轴平面坐标系中与x轴之间的夹角,默认0: 
@property CGFloat emissionLongitude; // 指定经度,经度角代表了在x-y轴平面坐标系中与x轴之间的夹角,默认0:
@property CGFloat emissionRange; //发射角度范围,默认0,以锥形分布开的发射角度。角度用弧度制。粒子均匀分布在这个锥形范围内;
@property CGFloat velocity; // 速度和速度范围,两者默认0
@property CGFloat velocityRange;
@property CGFloat xAcceleration; // x,y,z方向上的加速度分量,三者默认都是0
@property CGFloat yAcceleration;
@property CGFloat zAcceleration;
@property CGFloat scale; // 缩放比例, 默认是1
@property CGFloat scaleRange; // 缩放比例范围,默认是0
@property CGFloat scaleSpeed; // 在生命周期内的缩放速度,默认是0
@property CGFloat spin; // 粒子的平均旋转速度,默认是0
@property CGFloat spinRange; // 自旋转角度范围,弧度制,默认是0
@property(nullable) CGColorRef color; // 粒子的颜色,默认白色
@property float redRange; // 粒子颜色red,green,blue,alpha能改变的范围,默认0
@property float greenRange;
@property float blueRange;
@property float alphaRange;
@property float redSpeed; // 粒子颜色red,green,blue,alpha在生命周期内的改变速度,默认都是0
@property float greenSpeed;
@property float blueSpeed;
@property float alphaSpeed;
@property(nullable, strong) id contents; // 粒子的内容,为CGImageRef的对象
@property CGRect contentsRect;
@property CGFloat contentsScale;
@property(copy) NSString *minificationFilter;
@property(copy) NSString *magnificationFilter;
@property float minificationFilterBias;
@property(nullable, copy) NSArray<CAEmitterCell *> *emitterCells; // 粒子里面的粒子
@property(nullable, copy) NSDictionary *style;

CAEmitterCell里面的API里面的大部分属性作了说明,看看注释并调试一下就能理解大部分,接下来重点说说一些常用的属性。CAEmitterLayer就是粒子的工厂,但是要实现效果就需要CAEmitterCell的帮助。

1.CAEmitterCell决定生命状态的属性
  • lifetimelifetimeRange:粒子在系统上的生命周期,即存活时间,单位是秒。配合lifetimeRage来让粒子生命周期均匀变化,以便可以让粒子的出现和消失显得更加离散。
  • birthRate:每秒钟产生的粒子的数量,是浮点数。对于这个数量为浮点数,在测试的时候可以灵活使用它。比如你想看粒子的运动状态,但是太多了可能会很迷糊,这时候你把birthRate = 0.1f,其他参数不变,就能看到单个粒子的运动状态。
2.CAEmitterCell决定内容的属性
  • contents:为CGImageRef的对象。关于contents会联想到CALayer了,在CALayer中展示静态的图片是需要用到这个属性。提供一张图片,作为粒子系统的粒子。但是因为粒子系统可以给粒子上色,为了做出好的颜色变换效果,通常提供的图片为纯色的图片,一般为白色。
  • name:粒子的名字。初看没什么用,但是当CAEmitterLayer里面有很多个cell的时候,给每个cell设置好名字,要修改一些属性以达到动画效果的改变等,就可以通过KVC拿到这个cell的某个属性。在后面的几个demo中都用用到。
3.CAEmitterCell决定颜色状态的属性

粒子系统之所以能做出炫酷的效果,和它的色彩多样化有必不可上的关系,在CAEmitterCell中提供了较多的颜色控制属性这部分属性让你获得了控制粒子颜色,颜色变化范围和速度的能力,你可以凭借它来完成一些渐变的效果或其它构建在它之上的酷炫效果。接下来就看看这些颜色属性。

  • colorcolor是粒子的颜色属性,这个颜色属性的作用是给粒子上色,它的实现方法很简单,就是将contents自身的颜色的RGBA值 * color的RGBA值,就得到最终的粒子的颜色。为了很好的计算,通常用白色的图片作为contents,因为它的RGB都是255,转换为UIColor中的component就是1,用color乘上它就能得到color设置的颜色效果。
  • redRangegreenRangeblueRangealphaRange:这些是对应的color的RGBA的取值范围,取值范围为0~1,比如如下设置中
    snowCell.color = [[UIColor colorWithRed:0.1 green:0.2 blue:0.3 alpha:0.5]CGColor];
    snowCell.redRange = 0.1;
    snowCell.greenRange = 0.1;
    snowCell.blueRange = 0.1;
    snowCell.alphaRange = 0.1;

对应的RGBA的取值范围就是:R(0~0.2)、G(0.1~0.3)、B(0.2~0.4)、A(0.4~0.6)。

  • redSpeedgreenSpeedblueSpeedalphaSpeed:这些是对应的是粒子的RGBA的变化速度,取值范围为0~1。表示每秒钟的RGBA的变化率。这个变化率的计算方式其实很简单,先看下面的几行代码:
    snowCell.lifetime = 20.f;  // 粒子的生命周期
    snowCell.color = [[UIColor colorWithRed:0.f green:1.f blue:1.f alpha:1.f]CGColor];
    snowCell.redSpeed = 0.2;

这里设置了粒子颜色的RGBA,以及redSpeed,其他的没设置默认为0。粒子的生命周期(lifetime)为20秒,那么这个粒子从产生到消失的时间就是20秒。它的Red值为0,redSpeed为0.2,那么在粒子的这个生命周期内,粒子的每秒钟的Rde值就会增加0.2 * 255,表现在外观上的状态就是粒子颜色在不断变化,接近白色。最后粒子生命周期结束的时候,粒子的color正好是RGBA(1,1,1,1)。当然个变化的速度也可以负数,计算方式相同。比如要设置烟花的效果,那么要让在爆炸的过程中颜色变化,就是通过这样的设置达到的效果。

4.CAEmitterCell决定飞行轨迹的属性。

CAEmitterLayer虽然控制了粒子的发射位置和形状等,但是粒子的飞行同时也需要自身去决定,比如粒子发射的角度、发散的范围,自转属性等。那么接下来就说说这些属性。

  • emissionLongitude: 指定经度,经度角代表了在x-y轴平面坐标系中与x轴之间的夹角,默认0,弧度制。顺时针方向为正。这样解释看起来不好懂,画个图就明白了。
    emissionLatitude
    粒子沿着X轴向右飞行,如果emissionLongtitude = 0那么粒子会沿着X轴向右飞行,如果想沿着Y轴向下飞行,那么可以设置emissionLongtitude = M_PI_2
  • emissionLatitude:这个和emissionLongitude的原理是一样的,只不过是在三维平面上的x-z轴上与x轴的夹角。
  • emissionRange:发射角度范围,默认0,以锥形分布开的发射角度。角度用弧度制。粒子均匀分布在这个锥形范围内。在二维平面中,若想要以锥形的形式发射粒子,然粒子的发散范围不是一条线,而是一个锥形区域(也可以叫做扇形),那么可以通过emissionRange来设置一个范围。比如想沿Y轴向下成90度的锥形区域发散,那么可以通过如下代码设置:
    snowCell.emissionLongitude = M_PI_2;
    snowCell.emissionRange = M_PI_4;

实现的效果如下:

实现效果
可以看到粒子是沿着Y轴向下成90度的一个发散角度。如果想实现火焰等效果。就可以这样,把角度调小一点即可。

  • velocityvelocityRangexAccelerationyAccelerationzAcceleration:前面两个是粒子的初速度和初速度的范围,后面是三个分别是在x、y、z轴方向的加速度,这个很好理解,初中就应该知道加速度的概念,也就是每秒钟速度的变化量。在放烟花的效果中,烟花飞上天的过程中,模拟一个收重力影响的效果,就可以通过yAcceleration模拟一个重力加速度的效果。
  • spinspinRange:这两个属性很重要,是粒子的自转属性。在粒子被发射出去之后,想要实现自转,就需要用到他们。**粒子的自转是以弧度制来计算的,表示每秒钟粒子自转的弧度数。spin为正数代表粒子是顺时针旋转的,为负数的话就是逆时针选转了。**举个🌰:粒子的生命周期就是20秒,那么你想让你的粒子这个生命周期内刚好自转12周,若spinRange为0,那么粒子的spin值就应该为((PI/180)*360 * 2)/20,就得到了每秒需要转动的弧度数。
5.CAEmitterCell子粒子的属性
  • emitterCells:看到CAEmitterCell的这个属性的时候或许会有些疑惑,不用惊讶,前面说过CAEmitterLayer可以产生cell,通用cell也可以产生cell。那么这个属性就和CAEmitterLayer中的emitterCells一样,也是一个数组。这里有几个需要注意的地方:
    1. 若给cell设置了subCell,若想控制subCell的方向,那么得考虑父cell的方向属性,也就是emissionLongtitudeemissionLatitude这两个属性的情况。
    2. 不管父粒子cell是从什么样的形状上发射出来的,若要发射subCell,subCell总是从kCAEmitterLayerPoint形状上由父粒子的中心发射出来的。

##造几个小Demo

理解了CAEmitterLayerCAEmitterCell的属性之后,通过粒子系统实现一些炫酷的动画效果就很简单了。接下来对实现的几个小demo效果作个思路分享,欢迎提供更好的方法~。

QQ空间点赞动画

动画分析
  • 点赞的时候先是把按钮放大再缩小,这个可以用核心动画里面的关键帧动画CAKeyframeAnimation实现
  • 放大后马上回有一圈的粒子发射,这就是粒子系统实现
  • 然后从赞到取消赞没有动画效果
实现思路
  • 自定义按钮,重写setHighlighted方法去掉高亮状态。提供两张图片,用于默认状态和选中状态。
  • 配置发射源CAEmitterLayer和粒子CAEmitterCell。由于CAEmitterLayerbirthRate默认为1,CAEmitterCellbirthRate默认为0,那么先不设置这两个属性。给cell设置好name,然后自定义一个动画开始的方法,在这里面通过KVC设置CAEmitterCellbirthRate以实现动画
/**
 * 开始动画
 */
- (void)startAnimation{
    
    // 用KVC设置颗粒个数
    [self.explosionLayer setValue:@1000 forKeyPath:@"emitterCells.explosionCell.birthRate"];
    
    // 开始动画
    self.explosionLayer.beginTime = CACurrentMediaTime();
    // 延迟停止动画
    [self performSelector:@selector(stopAnimation) withObject:nil afterDelay:0.15];
}

/**
 * 动画结束
 */
- (void)stopAnimation{
    // 用KVC设置颗粒个数
    [self.explosionLayer setValue:@0 forKeyPath:@"emitterCells.explosionCell.birthRate"];
    [self.explosionLayer removeAllAnimations];
}
  • 重写按钮的setSelected方法,在里面通过关键帧动画实现缩放。
/**
 * 选中状态 实现缩放
 */
- (void)setSelected:(BOOL)selected{
    [super setSelected:selected];
    
    // 通过关键帧动画实现缩放
    CAKeyframeAnimation * animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"transform.scale";
    if (selected) {  // 从没有点击到点击状态 会有爆炸的动画效果
        animation.values = @[@1.5,@2.0, @0.8, @1.0];
        animation.duration = 0.5;
        
        animation.calculationMode = kCAAnimationCubic;
        [self.layer addAnimation:animation forKey:nil];

        // 让放大动画先执行完毕 再执行爆炸动画
        [self performSelector:@selector(startAnimation) withObject:nil afterDelay:0.25];
    }else{ // 从点击状态normal状态 无动画效果 如果点赞之后马上取消 那么也立马停止动画
        [self stopAnimation];
    }
}

结语

还有几个动画的思路就不啰嗦了,实现起来都很简单,关键是要去理解这些属性的作用。另外动画做起来即使费时间,要自己去理解属性的作用的话需要花时间调试对比,其实用其他方式也可以实现这样的效果,但是CAEmitterLayer 基于GPU,做这些效果的时候比较方便。demo已经上传github了,提供了下雨、下雪、红包雨、五彩小球、爱心、火焰、烟花等效果,值得一说的是,烟花需要三个cell,一个提供发射的shootCell,一个用于爆炸的explodeCell,还有一个用于火花的sparkCell,这些都是父子关系,一个cell生命周期完了另外一个再出来。这些可以查看代码体会,都有注释~。EmitterAnimation