ios 利用粒子动画实现今天头像点赞效果

image.png

CAEmitterLayer 是一个高性能的粒子引擎,被用来创建复杂的粒子动画如:烟雾,火,雨等效果,并且很好地控制了性能。

苹果给出的解释是:

CAEmitterLayer 看上去像是许多 CAEmitterCell 的容器,这些 CAEmitterCell 定义了一个例子效果。你将会为不同的例子效果定义一个或多个 CAEmitterCell 作为模版,同时 CAEmitterLayer 负责基于这些模版实例化一个粒子流。一个 CAEmitterCell 类似于一个 CALayer :它有一个 contents 属性可以定义为一个 CGImage ,另外还有一些可设置属性控制着表现和行为。

以上解释来源于网络

首先提醒CAEmitterLayer本身没有什么难度,主要在于两点:

  • 属性较多(一会会把属性都列举出来,不知道了随时查阅就是)
  • 调参数比较费时(想要有好的动画效果还得慢慢的去调整各项参数,不过没有难度就是有点费时间)
下面先认识一下CAEmitterLayer的属性
/* The center of the emission shape. Defaults to (0, 0, 0). Animatable. */
发射源位置。注意,是一个空间坐标。并且标记为 Animatable. 也就是说可以用 CoreAnimation 移动发射源位置
@property CGPoint emitterPosition;
@property CGFloat emitterZPosition;

“/* The size of the emission shape. Defaults to (0, 0, 0). Animatable.
 * Depending on the `emitterShape' property some of the values may be
 * ignored. */
发射源大小。注意除了宽和高之外,还有纵向深度。
文档中还提到,这两个属性有时候可能会因为设置了 emitterShape 而被忽略,具体情况实际尝试一下就可以了。
@property CGSize emitterSize;
@property CGFloat emitterDepth;

“/* A string defining the type of emission shape used. Current options are:
 * `point' (the default), `line', `rectangle', `circle', `cuboid' and
 * `sphere'. */
  
CA_EXTERN NSString * const kCAEmitterLayerPoint
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);
CA_EXTERN NSString * const kCAEmitterLayerLine
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);
CA_EXTERN NSString * const kCAEmitterLayerRectangle
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);
CA_EXTERN NSString * const kCAEmitterLayerCuboid
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);
CA_EXTERN NSString * const kCAEmitterLayerCircle
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);
CA_EXTERN NSString * const kCAEmitterLayerSphere
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);

 emitterShape 决定了发射源的形状。
@property(copy) NSString *emitterShape;
 
/* A string defining how particles are created relative to the emission
 * shape. Current options are `points', `outline', `surface' and
 * `volume' (the default). */
 

   
CA_EXTERN NSString * const kCAEmitterLayerPoints
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);
CA_EXTERN NSString * const kCAEmitterLayerOutline
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);
CA_EXTERN NSString * const kCAEmitterLayerSurface
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);
CA_EXTERN NSString * const kCAEmitterLayerVolume
    __OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_5_0);

 emitterMode 决定了发射源的发射模式。
@property(copy) NSString *emitterMode;

平常用的多的比如 emitterShape 的 kCAEmitterLayerLine 和 kCAEmitterLayerPoint。这两个从视觉上还是比较好区分的,这决定了你的粒子是从一个点「喷」出来的,还是从一条线上每个点「喷」下来,前者像焰火,后者像瀑布。显然,下雪的效果更像后者。

emitterMode 的 kCAEmitterLayerOutline 表示向外围扩散,如果你的发射源形状是 circle,那么 kCAEmitterLayerOutline 就会以一个圆的方式向外扩散开。

又比如你想表达一股蒸汽向上喷的效果,就可以设置 emitterShape 为 kCAEmitterLayerLine , emitterMode 为 kCAEmitterLayerOutline。

CAEmitterCell的属性

其实CAEmitterCell真是的名字叫粒子,下面详细的介绍了CAEmitterCell的属性,只要求大家属性一下,以后用到了可以再来查阅。

@property float birthRate; //每秒生成多少个粒子
 
@property float lifetime; //粒子存活的时间,以秒为单位
@property float lifetimeRange; // 可以为这个粒子存活的时间再指定一个范围。
上面两个属性如果只用了lifetime那么粒子的存活时间就是固定的,比如lifetime=10,那么粒子10s秒后就消失了。
如果使用了lifetimeRange,比如lifetimeRange=5,那么粒子的存活时间就是在5s~15s这个范围内消失。
 
@property CGFloat velocity;//粒子平均初始速度。正数表示竖直向上,负数竖直向下。
@property CGFloat velocityRange; //可以再指定一个范围。
上面两个属性同lifetime和lifetimeRange
 
@property CGFloat xAcceleration;
@property CGFloat yAcceleration;
@property CGFloat zAcceleration; //三者构成了一个空间矢量。决定了每个方向上粒子的加速度。
 
@property CGFloat emissionRange; //以锥形分布开的发射角度。角度用弧度制。粒子均匀分布在这个锥形范围内。
 
@property CGFloat spin;//粒子的平均旋转速度
@property CGFloat spinRange; //可指定一个范围。弧度制。
 
@property(strong) id contents; //cell的内容。通常是一个指针CGImageRef。
 
@property CGColorRef color; //可以把图片「染」成你想要的颜色。

@property(copy) NSString *name; //The name of the cell,用于构建key paths。这也是后面手动控制动画开始和结束的关键。

好,上面简单介绍了一下CAEmitterLayer和CAEmitterCell的一些基本属性,下面来利用粒子动画实现一个类似今日头条点赞效果。

#import <UIKit/UIKit.h>

@interface GXUpvoteButton : UIButton

@end
#import "GXUpvoteButton.h"

@interface GXUpvoteButton()

/**
 展示的layer
 */
@property (strong, nonatomic) CAEmitterLayer *streamerLayer;

/**
 图片数组
 */
@property (nonatomic, strong) NSMutableArray *imagesArr;

/**
 cell的数组
 */
@property (nonatomic, strong) NSMutableArray *CAEmitterCellArr;

/**
 展示多少个赞的label
 */
@property (nonatomic, strong) UILabel *zanLabel;

@end



@implementation GXUpvoteButton

{
    NSTimer *_timer; //定时器
    NSInteger countNum;//赞的个数
}

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder];
    if (self) {
        [self setup];
    }
    return self;
}

/**
 *  配置WclEmitterButton
 */
- (void)setup {
    //初始化 赞的个数
    countNum = 1;
    
    //展示多少个赞的label
    self.zanLabel = [[UILabel alloc]init];
    [self addSubview:self.zanLabel];
    self.zanLabel.frame = CGRectMake(-50 ,- 100, 200, 40);
    self.zanLabel.hidden = YES;
    
    //添加点击事件
    //点一下
    [self addGestureRecognizer:[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(pressOnece:)]];
    //长按
    [self addGestureRecognizer:[[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPress:)]];
    
    [self setImage:[UIImage imageNamed:@"feed_like"] forState:UIControlStateNormal];
    [self setImage:[UIImage imageNamed:@"feed_like_press"] forState:UIControlStateSelected];
    
    //设置暂时的layer
    _streamerLayer               = [CAEmitterLayer layer];
    _streamerLayer.emitterSize   = CGSizeMake(30, 30);
    _streamerLayer.masksToBounds = NO;
    _streamerLayer.renderMode = kCAEmitterLayerAdditive;
    [self.layer addSublayer:_streamerLayer];
}


/**
 点了一下

 @param ges 手势
 */
- (void)pressOnece:(UIGestureRecognizer *)ges
{
    UIButton * sender = (UIButton *)ges.view;
    sender.selected = !sender.selected;
    [self animation];
    [self performSelector:@selector(explode) withObject:nil afterDelay:0.1];
    if (sender.selected == NO) {
        //重置label文字
        countNum = 0;
        [self changeText];
        //清空数组
        [self.imagesArr removeAllObjects];
        [self.CAEmitterCellArr removeAllObjects];
    }
}


/**
 长按

 @param ges 手势
 */
- (void)longPress:(UIGestureRecognizer *)ges
{
    UIButton * sender = (UIButton *)ges.view;
    sender.selected = YES;
    if (ges.state == UIGestureRecognizerStateBegan) {
        [self animation];
    }else if (ges.state == UIGestureRecognizerStateEnded)
    {
        [self explode];
    }
}

/**
 *  开始动画
 */
- (void)animation {
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
    if (self.selected) {
        animation.values = @[@1.5 ,@0.8, @1.0,@1.2,@1.0];
        animation.duration = 0.5;
        [self startAnimate];
    }else
    {
        animation.values = @[@0.8, @1.0];
        animation.duration = 0.4;
    }
    animation.calculationMode = kCAAnimationCubic;
    [self.layer addAnimation:animation forKey:@"transform.scale"];
}

/**
 *  开始喷射
 */
- (void)startAnimate {
    
    for (int i = 1; i < 10; i++)
    {
        //78张图片 随机选9张
        int x = arc4random() % 77 + 1;
        NSString * imageStr = [NSString stringWithFormat:@"emoji_%d",x];
        [self.imagesArr addObject:imageStr];
    }
    
    //设置展示的cell
    for (NSString * imageStr in self.imagesArr) {
        CAEmitterCell * cell = [self emitterCell:[UIImage imageNamed:imageStr] Name:imageStr];
        [self.CAEmitterCellArr addObject:cell];
    }
    _streamerLayer.emitterCells  = self.CAEmitterCellArr;
    
    
    // 开启计时器 设置点赞次数的label
    self.zanLabel.hidden = NO;
    _timer = [NSTimer scheduledTimerWithTimeInterval:0.15 target:self selector:@selector(changeText) userInfo:nil repeats:YES];
    
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
    animation.values = @[@0.8, @1.0];
    animation.duration = 0.4;
    [self.zanLabel.layer addAnimation:animation forKey:@"transform.scale"];
    
    //_streamerLayer开始时间
    _streamerLayer.beginTime = CACurrentMediaTime();
    for (NSString * imgStr in self.imagesArr) {
        NSString * keyPathStr = [NSString stringWithFormat:@"emitterCells.%@.birthRate",imgStr];
        [_streamerLayer setValue:@7 forKeyPath:keyPathStr];
    }
    
}


/**
 *  停止喷射
 */
- (void)explode {
    //让chareLayer每秒喷射的个数为0个
    for (NSString * imgStr in self.imagesArr) {
        NSString * keyPathStr = [NSString stringWithFormat:@"emitterCells.%@.birthRate",imgStr];
        [self.streamerLayer setValue:@0 forKeyPath:keyPathStr];
    }
    _zanLabel.hidden = YES;
    [_timer invalidate];
    _timer = nil;
}



/**
 更改点赞个数label的文字
 */
- (void)changeText
{
    countNum ++;
    self.zanLabel.attributedText = [self getAttributedString:countNum];
    self.zanLabel.textAlignment = NSTextAlignmentCenter;
}


/**
 富文本设置label的图片内容
 
 @param num 当前赞的个数
 @return 要显示的富文本
 */
- (NSMutableAttributedString *)getAttributedString:(NSInteger)num
{
    //先把num 拆成个十百
    NSInteger ge = num % 10;
    NSInteger shi = num % 100 / 10;
    NSInteger bai = num % 1000 / 100;
    
    //大于1000则隐藏
    if (num >= 1000) {
        return nil;
    }
    
    
    NSMutableAttributedString * mutStr = [[NSMutableAttributedString alloc]init];
    
    //创建百位显示的图片
    if (bai != 0) {
        NSTextAttachment *b_attch = [[NSTextAttachment alloc] init];
        b_attch.image = [UIImage imageNamed:[NSString stringWithFormat:@"multi_digg_num_%ld",bai]];
        b_attch.bounds = CGRectMake(0, 0, b_attch.image.size.width, b_attch.image.size.height);
        NSAttributedString *b_string = [NSAttributedString attributedStringWithAttachment:b_attch];
        [mutStr appendAttributedString:b_string];
    }
    
    //创建十位显示的图片
    if (!(shi == 0 && bai == 0)) {
        NSTextAttachment *s_attch = [[NSTextAttachment alloc] init];
        s_attch.image = [UIImage imageNamed:[NSString stringWithFormat:@"multi_digg_num_%ld",shi ]];
        s_attch.bounds = CGRectMake(0, 0, s_attch.image.size.width, s_attch.image.size.height);
        NSAttributedString *s_string = [NSAttributedString attributedStringWithAttachment:s_attch];
        [mutStr appendAttributedString:s_string];
    }
    
    //创建个位显示的图片
    if (ge >= 0) {
        NSTextAttachment *g_attch = [[NSTextAttachment alloc] init];
        g_attch.image = [UIImage imageNamed:[NSString stringWithFormat:@"multi_digg_num_%ld",ge]];
        g_attch.bounds = CGRectMake(0, 0, g_attch.image.size.width, g_attch.image.size.height);
        NSAttributedString *g_string = [NSAttributedString attributedStringWithAttachment:g_attch];
        [mutStr appendAttributedString:g_string];
    }
    
    if (num <= 3) {
        //鼓励
        NSTextAttachment *attch = [[NSTextAttachment alloc] init];
        attch.image = [UIImage imageNamed:@"multi_digg_word_level_1"];
        attch.bounds = CGRectMake(0, 0, attch.image.size.width, attch.image.size.height);
        NSAttributedString *z_string = [NSAttributedString attributedStringWithAttachment:attch];
        [mutStr appendAttributedString:z_string];
    }else if (num <= 6)
    {
        //加油
        NSTextAttachment *attch = [[NSTextAttachment alloc] init];
        attch.image = [UIImage imageNamed:@"multi_digg_word_level_2"];
        attch.bounds = CGRectMake(0, 0, attch.image.size.width, attch.image.size.height);
        NSAttributedString *z_string = [NSAttributedString attributedStringWithAttachment:attch];
        [mutStr appendAttributedString:z_string];
    }else
    {
        //太棒了
        NSTextAttachment *attch = [[NSTextAttachment alloc] init];
        attch.image = [UIImage imageNamed:@"multi_digg_word_level_3"];
        attch.bounds = CGRectMake(0, 0, attch.image.size.width, attch.image.size.height);
        NSAttributedString *z_string = [NSAttributedString attributedStringWithAttachment:attch];
        [mutStr appendAttributedString:z_string];
    }
    
    return mutStr;
    
}



/**
 创建发射的表情cell
 
 @param image 传入随机的图片
 @param name 图片的名字
 @return cell
 */
- (CAEmitterCell *)emitterCell:(UIImage *)image Name:(NSString *)name
{
    CAEmitterCell * smoke = [CAEmitterCell emitterCell];
    smoke.birthRate = 0;//每秒出现多少个粒子
    smoke.lifetime = 2;// 粒子的存活时间
    smoke.lifetimeRange = 2;
    smoke.scale = 0.35;
    
    smoke.alphaRange = 1;
    smoke.alphaSpeed = -1.0;//消失范围
    smoke.yAcceleration = 450;//可以有下落的效果
    
    CGImageRef image2 = image.CGImage;
    smoke.contents= (__bridge id _Nullable)(image2);
    smoke.name = name; //设置这个 用来展示喷射动画 和隐藏
    
    smoke.velocity = 450;//速度
    smoke.velocityRange = 30;// 平均速度
    smoke.emissionLongitude = 3 * M_PI / 2 ;
    smoke.emissionRange = M_PI_2;//粒子的发散范围
    smoke.spin = M_PI * 2; // 粒子的平均旋转速度
    smoke.spinRange = M_PI * 2;// 粒子的旋转速度调整范围
    return smoke;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    //设置发射点的位置
    _streamerLayer.position = CGPointMake(self.frame.size.width/2.0, self.frame.size.height/2.0);
}


- (NSMutableArray *)imagesArr
{
    if (_imagesArr == nil) {
        _imagesArr = [NSMutableArray array];
    }
    return _imagesArr;
}

- (NSMutableArray *)CAEmitterCellArr
{
    if (_CAEmitterCellArr == nil) {
        _CAEmitterCellArr = [NSMutableArray array];
    }
    return _CAEmitterCellArr;
}

调用方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.upvoteButton = [GXUpvoteButton buttonWithType:UIButtonTypeCustom];
    [self.view addSubview:self.upvoteButton];
    self.upvoteButton.frame = CGRectMake(0, 0, 50, 50);
    self.upvoteButton.center = self.view.center;
}

具体demo稍后提供...

推荐阅读更多精彩内容