遮罩動畫/注水動畫
一般用CoreAnimation+mask 來實現(xiàn)其動畫效果。
mask指lyaer.mask屬性,等同于UIView的clipsToBounds屬性,將超出自身范圍外的內(nèi)容剪裁掉,不顯示。
來看動畫效果:

注水動畫的原理(很簡單的):
有兩個layer層,一個圖形層,一個遮罩層。用遮罩層去逐漸覆蓋背景圖形層,達到動畫的效果。
本著由易到難的原則,先看看最簡單的動畫如何去做。

這是一個類似進度條的動畫(丑了點),綠色是遮蓋層,橙色是背景圖形層??梢钥吹骄G色的遮罩層逐漸的覆蓋了橙色的背景層。類似進度由0到1的過程。
那么,這個動畫如何實現(xiàn)呢?
根據(jù)條件的不同,動畫實現(xiàn)的方式也不同:
- 不用控制進度,只要完成動畫就好
- 需要控制進度,還要控制動畫的快慢
首先,不需要控制進度的實現(xiàn)。在一定時間內(nèi),直接用遮罩層去覆蓋背景圖層好了。這里插一句:
Core Animation 維護了兩個平行 layer 層次結(jié)構(gòu): model layer tree(模型層樹) 和 presentation layer tree(表示層樹)。前者中的 layers 反映了我們能直接看到的 layers 的狀態(tài),而后者的 layers 則是動畫正在表現(xiàn)的值的近似。
當動畫運行結(jié)束后,model layer的值并不會被改變,所以添加動畫的圖層仍然會回到初始位置。但當我們想要動畫結(jié)束后,動畫停留在結(jié)束的位置,該怎么辦呢?兩種方法:
- 設(shè)定動畫removedOnCompletion屬性為NO,在圖層添加動畫之后(即[layer addAnimation:XXX forKey:ni]之后),手動修改model layer的結(jié)束位置,下面的示例就是用的這種方法.
- 圖層添加動畫之前,設(shè)定removedOnCompletion屬性為YES,同時設(shè)定animation.fillMode = kCAFillModeForward;
參考這里

keyPath=@"position.x"的實現(xiàn)方法:
//position.x animation
CALayer *pContainerLayer = [CALayer layer];
pContainerLayer.frame = CGRectMake(200, 50, 100, 50);
pContainerLayer.backgroundColor = [[UIColor orangeColor] CGColor];
[self.view.layer addSublayer:pContainerLayer];
CALayer *pCoverLayer = [CALayer layer];
pCoverLayer.frame = CGRectMake(0 - 100 , 0, 100, 50);
pCoverLayer.backgroundColor = [[UIColor greenColor] CGColor];
[pContainerLayer addSublayer:pCoverLayer];
CGFloat pToX = 100 / 2;
CABasicAnimation *pAnimation = [CABasicAnimation animationWithKeyPath:@"position.x"];;
pAnimation.fromValue = @(pCoverLayer.position.x);
pAnimation.toValue = @(pToX);
pAnimation.duration = 5.0f;
pAnimation.repeatCount = 1;
pAnimation.removedOnCompletion = YES;
[pCoverLayer addAnimation:pAnimation forKey:nil];

keyPath=@"bounds.size.width"的實現(xiàn)方法:寬度由 0->100
//bounds.size.width animation
CALayer *bContainerLayer = [CALayer layer];
bContainerLayer.frame = CGRectMake(200, 110, 100, 50);
bContainerLayer.backgroundColor = [[UIColor orangeColor] CGColor];
[self.view.layer addSublayer:bContainerLayer];
CALayer *bCoverLayer = [CALayer layer];
bCoverLayer.frame = CGRectMake(0, 0, 0, 50);
bCoverLayer.backgroundColor = [[UIColor greenColor] CGColor];
bCoverLayer.anchorPoint = CGPointMake(0, 0.5);
[bContainerLayer addSublayer:bCoverLayer];
CGFloat bToWidth = 100;
CABasicAnimation *bAnimation = [CABasicAnimation animationWithKeyPath:@"bounds.size.width"];
bAnimation.fromValue = @(0);
bAnimation.toValue = @(bToWidth);
bAnimation.duration = 5.0f;
bAnimation.repeatCount = 1;
bAnimation.removedOnCompletion = YES;
[bCoverLayer addAnimation:bAnimation forKey:nil];
// change model layer bounds
bCoverLayer.bounds = CGRectMake(0, 0, 100, 50);
其次,需要控制進度了,上面的方法就沒法使用了。

主要實現(xiàn)變化,其中速度就是slider從0到1的滑動速度。
slider: 0 -> 1
coverLayer width: 0 -> max(寬度的最大值)
@interface ViewController ()
@property (nonatomic,strong) CALayer *coverLayer;
@property (weak, nonatomic) IBOutlet UISlider *slider;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CALayer *sContainerLayer = [CALayer layer];
sContainerLayer.frame = CGRectMake(200, 110, 100, 50);
sContainerLayer.backgroundColor = [[UIColor orangeColor] CGColor];
[self.view.layer addSublayer:sContainerLayer];
CALayer *sCoverLayer = [CALayer layer];
sCoverLayer.frame = CGRectMake(0, 0, 0, 50);
sCoverLayer.backgroundColor = [[UIColor greenColor] CGColor];
sCoverLayer.anchorPoint = CGPointMake(0, 0.5);
[sContainerLayer addSublayer:sCoverLayer];
self.coverLayer = sCoverLayer;
[self.slider addTarget:self action:@selector(update:) forControlEvents:UIControlEventValueChanged];
}
//這里的slider.value * 100,100是背景圖層的寬度
- (void)update:(UISlider *)slider {
self.coverLayer.frame = CGRectMake(0, 0, slider.value * 100, 50);
}
@end
下面來個稍微復雜點的,加入layer的mask屬性:
- 一種背景圖層使用mask屬性,遮罩層不變,還是一個矩形,然后覆蓋的時候,只能顯示背景圖層的形狀,超出的部分被剪裁掉
- 另一種mask使用特殊圖形,遮蓋的時候,顯示的是mask的圖層的樣子

這是一個不規(guī)則圖形的注水動畫,其中背景圖層不規(guī)則,遮罩圖層是一個矩形。遮罩圖層顏色是白色,背景圖層是橙色。動畫的運動方向是,白色圖層從下到上,這里對應(yīng)的是size.heigh 由 max(94) -> 0 。
如何生成動畫中的圖形:
- CAShapeLayer利用屬性path,用貝塞爾創(chuàng)建需要的圖形。工具paintCode非常好用。
- 設(shè)定背景圖層的mask屬性為上一步生成的shapelayer。這樣就生成特定圖形的圖層了。
CALayer *canvasLayer = [CALayer layer];
canvasLayer.frame = CGRectMake(200, 80, 52, 94);
canvasLayer.backgroundColor = [[UIColor orangeColor] CGColor];
[self.view.layer addSublayer:canvasLayer];
CAShapeLayer *ovalShapeLayer = [CAShapeLayer layer];
ovalShapeLayer.path = [[self createBezierPath] CGPath];
canvasLayer.mask = ovalShapeLayer;
CALayer *coverLayer = [CALayer layer];
coverLayer.frame = CGRectMake(0, 0 , 52, 94 );
coverLayer.anchorPoint = CGPointMake(0, 0);
coverLayer.position = CGPointMake(0, 0);
coverLayer.backgroundColor = [[UIColor whiteColor] CGColor];
[canvasLayer addSublayer:coverLayer];
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"bounds.size.height";
animation.fromValue = @(94);
animation.toValue = @(0);
animation.duration = 5;
animation.repeatCount = HUGE;
animation.removedOnCompletion = YES;
[coverLayer addAnimation:animation forKey:nil];
貝塞爾生成的不規(guī)則圖形,frame為{1,1,52,94}
- (UIBezierPath *)createBezierPath {
// W:H = 70:120
// oval frame {1,1,52,94}
UIBezierPath* ovalPath = [UIBezierPath bezierPath];
[ovalPath moveToPoint: CGPointMake(53, 30.53)];
[ovalPath addCurveToPoint: CGPointMake(27, 95) controlPoint1: CGPointMake(53, 46.83) controlPoint2: CGPointMake(41.36, 95)];
[ovalPath addCurveToPoint: CGPointMake(1, 30.53) controlPoint1: CGPointMake(12.64, 95) controlPoint2: CGPointMake(1, 46.83)];
[ovalPath addCurveToPoint: CGPointMake(27, 1) controlPoint1: CGPointMake(1, 14.22) controlPoint2: CGPointMake(12.64, 1)];
[ovalPath addCurveToPoint: CGPointMake(53, 30.53) controlPoint1: CGPointMake(41.36, 1) controlPoint2: CGPointMake(53, 14.22)];
[ovalPath closePath];
return ovalPath;
}
再來看看波濤洶涌的動畫:

這是一個不斷波動并上升的動畫。動畫主要由兩部分組成:
- 時刻滾動的波浪
- 不斷上升的水平面
時刻滾動的波浪:創(chuàng)建一個CAShapeLayer層,作為mask。使用CADisplayLink,每楨都重畫shapeLayer的path屬性,這樣子就會產(chǎn)生波浪起伏的效果。

不斷上升的水平面,是利用CABasicAnimation動畫,修改masklayerd的position的結(jié)果。

再說下這里圖層的包含關(guān)系,左面包含右面圖層:view.lyaer->bglayer->canvasLayer->waveLayer
view.layer是當前控制器的view的layer,
bglayer是黑色邊框圖層,
canvasLayer是背景圖層,
waveLayer是遮罩圖層
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *displayLink;
//背景圖層
@property (nonatomic, strong) CALayer *canvasLayer;
//遮罩圖層
@property (nonatomic, strong) CAShapeLayer *waveLayer;
//背景圖層frame
@property (nonatomic) CGRect frame;
//遮罩圖層frame
@property (nonatomic) CGRect shapeFrame;
@end
@implementation ViewController
//初始相位
static float phase = 0;
//相位偏移量
static float phaseShift = 0.25;
- (void)viewDidLoad {
[super viewDidLoad];
//shapePointY=200,設(shè)定mask其實位置(0,200)
CGFloat shapePointY = 200;
CGRect frame = CGRectMake(0, 0, 100, 200);
CGRect shapeFrame = CGRectMake(0, shapePointY, 100, 200);
self.frame = frame;
self.shapeFrame = shapeFrame;
//黑色邊框
CALayer *bglayer = [CALayer layer];
bglayer.frame = CGRectMake(0, 20, 100, 200);
bglayer.borderWidth = 1.0;
bglayer.borderColor = [[UIColor blackColor] CGColor];
[self.view.layer addSublayer:bglayer];
//創(chuàng)建背景圖層
self.canvasLayer = [CALayer layer];
self.canvasLayer.frame = frame;
self.canvasLayer.backgroundColor = [UIColor orangeColor].CGColor;
[bglayer addSublayer:self.canvasLayer];
//創(chuàng)建遮罩圖層
self.waveLayer = [CAShapeLayer layer];
self.waveLayer.frame = shapeFrame;
//設(shè)定mask為waveLayer
self.canvasLayer.mask = self.waveLayer;
//開始動畫
[self startAnimating];
}
- (void)startAnimating {
[self.displayLink invalidate];
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
//獲得結(jié)束時的position.y值
CGPoint position = self.waveLayer.position;
position.y = position.y - self.shapeFrame.size.height;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.fromValue = [NSValue valueWithCGPoint:self.waveLayer.position];
animation.toValue = [NSValue valueWithCGPoint:position];
animation.duration = 5.0;
animation.repeatCount = HUGE_VALF;
animation.removedOnCompletion = NO;
[self.waveLayer addAnimation:animation forKey:nil];
}
//波浪滾動 phase相位每楨變化值:phaseShift
- (void)update {
CGRect frame = self.frame;
phase += phaseShift;
UIGraphicsBeginImageContext(frame.size);
UIBezierPath *wavePath = [UIBezierPath bezierPath];
//用UIBezierPath畫一個閉合的路徑
CGFloat endX = 0;
for(CGFloat x = 0; x < frame.size.width ; x += 1) {
endX=x;
//正弦函數(shù),求y值
CGFloat y = 5 * sinf(2 * M_PI *(x / frame.size.width) + phase) ;
if (x==0) {
[wavePath moveToPoint:CGPointMake(x, y)];
}else {
[wavePath addLineToPoint:CGPointMake(x, y)];
}
}
CGFloat endY = CGRectGetHeight(frame);
[wavePath addLineToPoint:CGPointMake(endX, endY)];
[wavePath addLineToPoint:CGPointMake(0, endY)];
//修改每楨的wavelayer.path
self.waveLayer.path = [wavePath CGPath];
UIGraphicsEndImageContext();
}
@end
最后,讓波浪在特定容器中運動。即文章開始的動效。思路有兩種:
- 直接添加一個背景圖片到bglayer上
- 將bglayer設(shè)定為CAShapeLayer,path為想要圖形的值,動畫在CAShpaeLayer中顯示。
最后Demo見這里