草庐IT

CAEmitterLayer动画的开始和结束

MrYu4 2023-03-28 原文

有个需求,要求模仿微信做表情下雨的动画,一开始想用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];
        }
    });

这时候我们能看到效果如此:

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

  • 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];
        }
    });

效果图:

有关CAEmitterLayer动画的开始和结束的更多相关文章

  1. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  2. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

  3. ruby - 如果满足给定条件,则结束 ruby​​ 程序 - 2

    基本上,我只是试图在满足特定条件时停止程序运行其余行。unlessraw_information.firstputs"Noresultswerereturnedforthatquery"breakend然而,在程序运行之前我得到了这个错误:Invalidbreakcompileerror(SyntaxError)执行此操作的正确方法是什么? 最佳答案 abort("Noresultswerereturnedforthatquery")unlesscondition或unlessconditionabort("Noresultswer

  4. ruby-on-rails - Ruby/Rails 中的夏令时开始和结束日期 - 2

    我正在开发一个Rails应用程序,我需要在其中找到给定特定偏移量或时区的夏令时开始和结束日期。我基本上在我的数据库中保存了从用户浏览器接收到的时区偏移量(“+3”,“-5”),我想在它出现时修改它由于夏令时的变化。我知道Time实例变量有dst?和isdst方法,如果存储在它们中的日期在夏令时与否。>Time.new.isdst=>true但是使用它来查找夏令时的开始和结束日期会占用太多资源,而且我还必须为我拥有的每个时区偏移量执行此操作。我想知道更好的方法。 最佳答案 好的,基于你所说的和@dhouty'sanswer:您希望能够

  5. ruby-on-rails - phusion passenger 和 ruby​​ 1.9.1 已经开始工作了吗? - 2

    我有一台生产机器和一台开发机器,都运行ubuntu8.10并且都运行最新的phusionpassenger。当我在osx上的本地开发机器上使用ruby​​1.9.1时,我想知道外面的人是否已经在使用带有ruby​​1.9.1甚至1.9.2的phusionpassenger?如果是这样,请告诉我们您的设置!此外,有没有办法在apache上使用phusionpassenger同时运行ruby​​1.8.7(ree)和1.9.1?感谢您的指点,我在任何地方都找不到任何提示... 最佳答案 是的,从某些2.2.x版本开始就正式支持它,我不记

  6. ruby - Rails 3 - 我可以将开始日期设置为 date_select 方法吗? - 2

    date_select方法只能设置:start_year,但我想设置开始日期(例如3个月前的日期)(但没有这样的选项)。那么,我可以将开始日期设置为date_select方法吗?或者,要制作这样的选择框,我应该使用select_tag和options_for_select吗?或者,有什么解决办法吗?谢谢, 最佳答案 有可能……例如:start_year–设置年份选择的开始年份。默认为Time.now.year-5参见thisresource. 关于ruby-Rails3-我可以将开始日期

  7. ruby - 如何在 watir 测试套件结束时关闭浏览器? - 2

    使用ruby​​的watir测试网络应用程序时,浏览器最后会保持打开状态。网上的一些建议是,要进行真正的单元测试,您应该在每次测试时(在拆卸调用中)打开和关闭浏览器,但这很慢而且毫无意义。或者他们做这样的事情:defself.suites=superdefs.afterClass#Closebrowserenddefs.run(*args)superafterClassendsend但这会导致摘要输出不再显示(诸如“100次测试、100次断言、0次失败、0次错误”之类的内容仍应显示)。我怎样才能让ruby​​或watir在我的测试结束时关闭浏览器? 最佳答案

  8. ruby - 从结束值创建一系列字符串 - 2

    我使用irb。下面是我写的代码。“斧头”..“bc”我期待"ax""ay""az""ba"bb""bc"但结果只是“斧头”..“bc”我该如何纠正?谢谢。 最佳答案 >puts("ax".."bc").to_aaxayazbabbbc 关于ruby-从结束值创建一系列字符串,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/7617092/

  9. ruby - 从特定索引开始迭代数组 - 2

    我想从特定索引开始遍历数组。我该怎么做?myj.eachdo|temp|...end 最佳答案 执行以下操作:your_array[your_index..-1].eachdo|temp|###end 关于ruby-从特定索引开始迭代数组,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/44151758/

  10. LVGL V8动画 - 2

    动画/*INITIALIZEANANIMATION 初始化一个动画*-----------------------*/lv_anim_ta;lv_anim_init(&a);/*MANDATORYSETTINGS 必选设置*------------------*//*Setthe"animator"function 设置“动画”功能*/lv_anim_set_exec_cb(&a,(lv_anim_exec_xcb_t)lv_obj_set_x);/*Setthe"animator"function*/lv_anim_set_var(&a,obj);/*Lengthoftheanim

随机推荐