评论

收藏

[iOS开发] CAEmitterLayer动画的开始和结束

移动开发 移动开发 发布于:2022-03-03 12:42 | 阅读数:523 | 评论:0

有个需求,要求模仿微信做表情下雨的动画,一开始想用CAEmitterLayer,实现的代码如下:
//期望:显示特效五秒后结束特效
  UIImage *image = [UIImage imageNamed:@"snow_white"];
  CGRect endRect = self.view.frame;
  UIView *layerView = [[UIView alloc]initWithFrame:endRect];
  //方便看到有view增加到里面
  layerView.backgroundColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:0.1];
  layerView.userInteractionEnabled = NO;
  CAEmitterLayer * fireworksLayer = [CAEmitterLayer layer];
  [layerView.layer addSublayer:fireworksLayer];
  [self.view addSubview:layerView];
  [layerView.layer addSublayer:fireworksLayer];
  fireworksLayer.emitterPosition = CGPointMake(endRect.size.width * 0.5, -image.size.height / 2); // 这个position表示的是粒子产生的位置,注意是图片中心位置的初始值,而不是(x,y)
  fireworksLayer.emitterSize = CGSizeMake(endRect.size.width - image.size.width / 2 * 2, 0.f);  // 粒子产生的随机区域,如果要让生成的粒子图片不过左右屏幕边缘,记住给左右两边留点空间
  fireworksLayer.emitterMode = kCAEmitterLayerOutline;
  fireworksLayer.emitterShape = kCAEmitterLayerLine;
  fireworksLayer.renderMode = kCAEmitterLayerAdditive;
  fireworksLayer.birthRate = 1;
  
  // 粒子
  CAEmitterCell * cell = [CAEmitterCell emitterCell];
  cell.birthRate = 1.f;//每一秒产生的粒子个数(实际数量和上面的birthRate相乘)
  cell.lifetime = 5.f;//粒子产生到消失为5秒
  cell.velocity = -(endRect.size.height + image.size.height / 2 * 2) / 5;
  cell.contents = (id)[image CGImage];
  
  fireworksLayer.emitterCells = @[cell];
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5), dispatch_get_main_queue(), ^{
    //移除layer
    if (layerView.superview) {
      [layerView removeFromSuperview];
    }
  });
这时候我们能看到效果如此:
DSC0000.gif

这个时候其实我们做到了以下几点

  • view显示5秒并删除
  • 雪花从上向下降落,而且速度是匀速的view高度/5秒
  • 雪花每秒产生1颗
  • 雪花在不超出左右边缘之内随机产生
但是这个效果最大的缺陷在于:雪花的产生和结束都十分的突兀,还需要实现的效果应该是:

  • 效果开始时,雪花正好从顶部开始落下
  • view即将移除的时候,雪花不再生成
  • view移除的时候,最后一片雪花刚好落出屏幕
所以我们的优化也从三个方面进行。
动画起始时间点 beginTime
如果直接查看CAEmitterLayer.h文件,并不能发现beginTime这个属性,甚至其父类CALayer也找不到,直到找到协议CAMediaTiming才能找到。
// CALayer.h
@interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>
...
@end
// CAMediaTiming.h
@protocol CAMediaTiming
...
/* The begin time of the object, in relation to its parent object, if
 * applicable. Defaults to 0. */
@property CFTimeInterval beginTime;
...
@end
所以我们设置beginTime就可以规定我们想让layer开始动画的时间,但是不应该设置为0(本来就是默认为0),而是CACurrentMediaTime(),也就是现在。
CACurrentMediaTime,文档里面告诉我们这是一个以秒为单位的当前的absolute time,属性为CFTimeInterval(也是double), 在这里“绝对时间”不是某度百科里的提到的时空观或者某个历法,而是mach_absolute_time()——即基于系统启动后的时钟嘀嗒数转换单位为秒的结果,与常见的时间戳[[NSDate date]timeIntervalSince1970]更是没半毛钱没有关系。
   NSLog(@"%lf %lf",[[NSDate date]timeIntervalSince1970], CACurrentMediaTime());
   //在某个时空输出为:1646246718.232236 197452.774114
回到问题来,我们只要设置好beginTime,就可以让动画从“现在”开始播放,而默认的0表示的是在“万物之始”开始播放,也难怪我们刚才从屏幕上开始看到的动画不是从零开始的。
fireworksLayer.beginTime = CACurrentMediaTime();
让雪花停止生成
我们需要在粒子开始运动之后,让粒子生成速度更改为0个/秒,首先明确的是,每秒生成粒子实际数量是fireworksLayer.birthRate * cell.birthRate,那么我们挠挠头就可以添加这段代码。
//细化需求为1s-3s是不断生成粒子,3s生成最后一个粒子后不再生成粒子
  .....
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3), dispatch_get_main_queue(), ^{
    fireworksLayer.birthRate = 0;//动画在运动过程中要修改参数的话,可以修改layer的参数,试过在这个过程中只改cell参数不会成功
  });
调整速度,让最后一颗粒子正好滚出屏幕时移除view
这里为了方便说明,所以本次只让雪花在y轴上做匀速运动,在x轴上相对静止(不左右移动),也是提醒各位,粒子产生的位置不是图片初始的xy坐标值,而是中心点,这既是是我代码中设置fireworksLayer.emitterPosition会附加图片一半的高度(开始生成粒子时粒子下沿要在view上端)的原因,也是为什么计算速度所用到的总路程时,要加上图片高度一半的两倍(粒子上沿要到view下端才算结束)的原因。
cell.velocity = -(endRect.size.height + image.size.height / 2 * 2) / 2; //正好最后一朵花开始的时候其图片下沿在屏幕顶部,结束的时候其图片上落到屏幕顶部
加上文章内提交优化后的代码:
//期望:显示特效五秒后结束特效
  UIImage *image = [UIImage imageNamed:@"snow_white"];
  CGRect endRect = self.view.frame;
  UIView *layerView = [[UIView alloc]initWithFrame:endRect];
  //方便看到有view增加到里面
  layerView.backgroundColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:0.1];
  layerView.userInteractionEnabled = NO;
  CAEmitterLayer * fireworksLayer = [CAEmitterLayer layer];
  [layerView.layer addSublayer:fireworksLayer];
  [self.view addSubview:layerView];
  [layerView.layer addSublayer:fireworksLayer];
  fireworksLayer.emitterPosition = CGPointMake(endRect.size.width * 0.5, -image.size.height / 2); // 这个position表示的是粒子产生的位置,注意是图片中心位置的初始值,而不是(x,y)
  fireworksLayer.emitterSize = CGSizeMake(endRect.size.width - image.size.width / 2 * 2, 0.f);  // 粒子产生的随机区域,如果要让生成的粒子图片不过左右屏幕边缘,记住给左右两边留点空间
  fireworksLayer.emitterMode = kCAEmitterLayerOutline;
  fireworksLayer.emitterShape = kCAEmitterLayerLine;
  fireworksLayer.renderMode = kCAEmitterLayerAdditive;
  fireworksLayer.birthRate = 1;
  fireworksLayer.beginTime = CACurrentMediaTime();
  
  // 粒子
  CAEmitterCell * cell = [CAEmitterCell emitterCell];
  cell.birthRate = 1.f;//每一秒产生的粒子个数(实际数量和上面的birthRate相乘)
  cell.lifetime = 5.f;//粒子产生到消失为5秒
  cell.velocity = -(endRect.size.height + image.size.height / 2 * 2) / 2; //正好最后一朵花开始的时候其图片下沿在屏幕顶部,结束的时候其图片上落到屏幕顶部
  cell.contents = (id)[image CGImage];
  
  fireworksLayer.emitterCells = @[cell];
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3), dispatch_get_main_queue(), ^{
    fireworksLayer.birthRate = 0;//动画在运动过程中要修改参数的话,可以修改layer的参数,试过在这个过程中只改cell参数不会成功
  });
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5), dispatch_get_main_queue(), ^{
    //移除layer
    if (layerView.superview) {
      [layerView removeFromSuperview];
    }
  });
效果图:
DSC0001.gif



   
   
   
                        

关注下面的标签,发现更多相似文章