CoreAnimation动画(一):遮罩动画/注水动画

遮罩动画/注水动画

一般用CoreAnimation+mask 来实现其动画效果。
mask指lyaer.mask属性,等同于UIView的clipsToBounds属性,将超出自身范围外的内容剪裁掉,不显示。

来看动画效果:


注水动画

注水动画的原理(很简单的):
有两个layer层,一个图形层,一个遮罩层。用遮罩层去逐渐覆盖背景图形层,达到动画的效果。

本着由易到难的原则,先看看最简单的动画如何去做。

easymaskanimating.gif

这是一个类似进度条的动画(丑了点),绿色是遮盖层,橙色是背景图形层。可以看到绿色的遮罩层逐渐的覆盖了橙色的背景层。类似进度由0到1的过程。
那么,这个动画如何实现呢?
根据条件的不同,动画实现的方式也不同:

  • 不用控制进度,只要完成动画就好
  • 需要控制进度,还要控制动画的快慢

首先,不需要控制进度的实现。在一定时间内,直接用遮罩层去覆盖背景图层好了。这里插一句:

Core Animation 维护了两个平行 layer 层次结构: model layer tree(模型层树) 和 presentation layer tree(表示层树)。前者中的 layers 反映了我们能直接看到的 layers 的状态,而后者的 layers 则是动画正在表现的值的近似。

当动画运行结束后,model layer的值并不会被改变,所以添加动画的图层仍然会回到初始位置。但当我们想要动画结束后,动画停留在结束的位置,该怎么办呢?两种方法:

  • 设定动画removedOnCompletion属性为NO,在图层添加动画之后(即[layer addAnimation:XXX forKey:ni]之后),手动修改model layer的结束位置,下面的示例就是用的这种方法.
  • 图层添加动画之前,设定removedOnCompletion属性为YES,同时设定animation.fillMode = kCAFillModeForward;

参考这里

position动画

keyPath=@"position.x"的实现方法:

    //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];

width动画

keyPath=@"bounds.size.width"的实现方法:宽度由 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);

其次,需要控制进度了,上面的方法就没法使用了。

手动控制进度动画

主要实现变化,其中速度就是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的图层的样子
注水动画

这是一个不规则图形的注水动画,其中背景图层不规则,遮罩图层是一个矩形。遮罩图层颜色是白色,背景图层是橙色。动画的运动方向是,白色图层从下到上,这里对应的是size.heigh 由 max(94) -> 0 。
如何生成动画中的图形:

  • CAShapeLayer利用属性path,用贝塞尔创建需要的图形。工具paintCode非常好用。
  • 设定背景图层的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];

贝塞尔生成的不规则图形,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;
}

再来看看波涛汹涌的动画:

注水动画矩形背景

这是一个不断波动并上升的动画。动画主要由两部分组成:

  • 时刻滚动的波浪
  • 不断上升的水平面

时刻滚动的波浪:创建一个CAShapeLayer层,作为mask。使用CADisplayLink,每桢都重画shapeLayer的path属性,这样子就会产生波浪起伏的效果。

波形动画

不断上升的水平面,是利用CABasicAnimation动画,修改masklayerd的position的结果。


上升动画

再说下这里图层的包含关系,左面包含右面图层: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,设定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];
    
    //创建背景图层
    self.canvasLayer = [CALayer layer];
    self.canvasLayer.frame = frame;
    self.canvasLayer.backgroundColor = [UIColor orangeColor].CGColor;
    [bglayer addSublayer:self.canvasLayer];
    //创建遮罩图层
    self.waveLayer = [CAShapeLayer layer];
    self.waveLayer.frame = shapeFrame;
    //设定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];
    
    //获得结束时的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;
        //正弦函数,求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设定为CAShapeLayer,path为想要图形的值,动画在CAShpaeLayer中显示。

最后Demo这里

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容

  • 转载:http://www.jianshu.com/p/32fcadd12108 每个UIView有一个伙伴称为l...
    F麦子阅读 6,202评论 0 13
  • 每个UIView有一个伙伴称为layer,一个CALayer。UIView实际上并没有把自己画到屏幕上;它绘制本身...
    shenzhenboy阅读 3,107评论 0 17
  • 书写的很好,翻译的也棒!感谢译者,感谢感谢! iOS-Core-Animation-Advanced-Techni...
    钱嘘嘘阅读 2,297评论 0 6
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,490评论 6 30
  • 沙盒环境就是为了和外部环境进行隔离,把每个程序都封装在不同盒子里面,以保证每个程序的python环境都是独立的 把...
    又不行了阅读 1,031评论 0 0