1. 程式人生 > 其它 >CAEmitterLayer動畫的開始和結束

CAEmitterLayer動畫的開始和結束

有個需求,要求模仿微信做表情下雨的動畫,一開始想用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];
        }
    });

效果圖: