1. 贝塞尔曲线
1.1 贝塞尔曲线反转
- 如果不在 CAShaperLayer 里操作,那么就只能在 UIView 的
-drawRect
下操作才会显示 - 反转 : 起始点和终点的位置对调
- (void)drawRect:(CGRect)rect {
//反转 起始点和终点的反转
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(20.f, 100.f)];
[path addLineToPoint:CGPointMake(250.f, 100.f)];
//注意这里提前生成了反转的路径,再回到原路径操作
UIBezierPath *path1 = [path bezierPathByReversingPath];
//这里是path 不是path1
[path addLineToPoint:CGPointMake(300.f, 200.f)];
path.lineWidth = 3.f;
[[UIColor redColor] set];
[path stroke];
//向下平移200,防止重合看不出效果
CGAffineTransform tranform = CGAffineTransformMakeTranslation(0.f, 200.f);
[path1 applyTransform:tranform];
[path1 addLineToPoint:CGPointMake(300.f, 200.f)];
path1.lineWidth = 3.f;
[[UIColor redColor] set];
[path1 stroke];
}
1.2 多重贝塞尔路径
这里不能用
-add
(比如内部画圆的情况),因为-add
会从绘制完矩形的终点,再描一条线到圆形绘制的起点,实际上是一条路径,而现在我们需要的效果是由两条路径组成-
使用
-add
方法(多了一条线)
[path addArcWithCenter:CGPointMake(150.f, 150.f) radius:30 startAngle:0 endAngle:M_PI*2 clockwise:YES];
使用
-appendPath
方法 (双路径)
- 如果我们单纯的填充颜色,情况如下(全填充):
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(100.f, 100.f, 100.f, 100.f) cornerRadius:1];
[path appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(150.f, 150.f) radius:30 startAngle:0 endAngle:M_PI clockwise:YES] ];
CAShapeLayer *layer1 = [CAShapeLayer layer];
layer1.path = path.CGPath;
layer1.strokeColor = [UIColor lightGrayColor].CGColor;
layer1.fillColor = [UIColor redColor].CGColor;
[self.layer addSublayer:layer1];
- 如果需要局部填充,将内部圆路径反转:
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(100.f, 100.f, 100.f, 100.f) cornerRadius:1];
//path2 是反转的
[path appendPath:[[UIBezierPath bezierPathWithArcCenter:CGPointMake(150.f, 150.f) radius:30 startAngle:0 endAngle:M_PI clockwise:YES] bezierPathByReversingPath]];
CAShapeLayer *layer1 = [CAShapeLayer layer];
layer1.path = path.CGPath;
layer1.strokeColor = [UIColor lightGrayColor].CGColor;
layer1.fillColor = [UIColor redColor].CGColor;
[self.layer addSublayer:layer1];
1.3 贝塞尔虚线
for (int i=0; i<20.f; i++) {
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(100.f, 100.f+20*i)];
[path addLineToPoint:CGPointMake(250.f, 100.f+20*i)];
path.lineWidth = 3.f;
[[UIColor redColor] set];
CGFloat arr[] = {8.f, 4.f, 16.f, 8.f};
[path setLineDash:arr count:4 phase:i];
[path stroke];
}
- count 这条虚线有几组
- phase 虚线起始位置样式
- arr 表示每小组虚线里面每一小段的长度(8.f红色 4.f白色 16.f红色 8.f白色)
2. BaseAnimation
2.1 基本属性介绍
- 关于动画过程的实时监听
- 一般情况下我们无法监听动画中实时变化的值,如frame。所以我们需要了解下面的机制
- layer 的动画由 model层 与 presentationlayer 组成,model层即是layer原始的frame等属性
- 动画的过程中,对于layer而言:model层不变,改变的只是presentationlayer的值(只有在动画开始的时候才有的)
- FillMode
/*
* 选择bothMode,那么动画会从直接从fromValue的位置开始
* 选择forwardsMode,那么动画会从初始frame移动到fromValue的位置 ,再从fromValue移动到toValue
*/
- CACurrentMediaTime
一个专门服务于CA的绝对时间,可用于解决多个动画协同性的问题
- 一个简单的 BaseAnimation 作为讲解属性的示例:
layer = [EOCShaperLayer layer];
layer.frame = CGRectMake(50.f, 50.f, 100.f, 100.f);
layer.delegate = self;
layer.backgroundColor = [UIColor lightGrayColor].CGColor;
[self.view.layer addSublayer:layer];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"newPosition"];
animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(300.f, 300.f)];
animation.beginTime = CACurrentMediaTime() + 1.f; //一个专门服务于CA的绝对时间,可用于解决多个动画的同步性与相对时间的问题
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(200.f, 200.f)];
// layer.position = CGPointMake(200.f, 200.f);
animation.removedOnCompletion = NO;
/*
* 选择bothMode,那么动画会从直接从fromValue的位置开始
* 选择forwardsMode,那么动画会从初始frame移动到fromValue的位置 ,再从fromValue移动到toValue
*/
animation.fillMode = kCAFillModeBoth;
NSLog(@"%@", layer.presentationLayer);
animation.duration = 3.f;
[layer addAnimation:animation forKey:nil];
2.2 监听动画的实时属性变化
- 上面的代码为何是
newPosition
?
- 经实践发现,我们监听系统的position并不会实时显示坐标
- 所以我们在layer层自定义了一个 newPosition 的属性
@interface EOCShaperLayer : CAShapeLayer
@property(nonatomic, assign)CGPoint newPosition;
@end
/*
* 系统的某个key值变化是否需要调用 display 方法
*/
+ (BOOL)needsDisplayForKey:(NSString *)key {
if ([key isEqualToString:@"newPosition"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
/*
* display 1秒调用60次 (fps = 60)
*/
- (void)display {
// self的属性 就是 presentationLayer 的属性
// presentationLayer 本质就是动画时生成的 layer(self)
self.position = self.presentationLayer.newPosition;
// 由于系统内部做了处理,我们打印 position 是不会显示值的
NSLog(@"newPosition%@", NSStringFromCGPoint(self.presentationLayer.newPosition));
}
@end
2.3 subLayer 需要手动释放的特例
- 在 Controller 生命周期结束的时候(
-dealloc
),有两种情况下 subLayer 需要手动释放 - 设置了 subLayer 的 delegate
- 使用了 layer 的 display 方法
3. 关键帧动画
- 多阶段贝塞尔曲线
- 一个飞机沿着路径运动的动画
CAShapeLayer *pathLayer = [CAShapeLayer layer];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(40.f, 175.f)];
[path addCurveToPoint:CGPointMake(300.f, 175.f) controlPoint1:CGPointMake(50.f, 40.f) controlPoint2:CGPointMake(200.f, 300.f)];
pathLayer.path = path.CGPath;
pathLayer.lineWidth = 2.f;
pathLayer.fillColor = [UIColor clearColor].CGColor;
[pathLayer setStrokeColor:[UIColor redColor].CGColor];
[self.view.layer addSublayer:pathLayer];
shapeLayer = [CAShapeLayer layer];
shapeLayer.contents = (__bridge id)[UIImage imageNamed:@"plane.png"].CGImage;
shapeLayer.bounds = CGRectMake(0.f, 0.f, 50.f, 50.f);
shapeLayer.position = CGPointMake(40.f, 175.f);
[self.view.layer addSublayer:shapeLayer];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
[shapeLayer addAnimation:anim forKey:@"eoc"];
- 在这里有一个缺陷,飞机的机头一直没有方向上的变化
- 飞行优化方式
anim.rotationMode = kCAAnimationRotateAuto;
- 关键帧动画 anim.values
- 会偏离出路径,移动到设置的关键帧上
- 如果不设置 keyTimes 那么动画轨迹都是平滑的
anim.values = @[[NSValue valueWithCGPoint:CGPointMake(100, 100)], [NSValue valueWithCGPoint:CGPointMake(200, 300)], [NSValue valueWithCGPoint:CGPointMake(50, 500)]];
anim.keyTimes = @[@0, @0.75, @1]; //keyTimes可以不设置,每个元素的时间点对应每个关键帧,每一个值都是要小于1
- anim.calculationMode
- <动画匀速播放模式>,如果在keyTimes设置的时间比较突兀,那么在设置了Paced后可以缓冲这种影响
anim.calculationMode = kCAAnimationCubicPaced;
anim.autoreverses
anim.autoreverses = YES
自动反转动画,类似倒带anim.beginTime
- 必须要加 CACurrentMediaTime() ,这表示的一个系列用于动画层面的一个独立的时间单位
anim.beginTime = CACurrentMediaTime() + 2.f; // 表示延时两秒开始
- anim.timeOffset
- 表示从动画整个时间轴中的 偏移到第 1 秒开始播放;
- 比如动画长5秒,timeOffset = 1.f,实际播放的是后4秒;
anim.timeOffset = 1.f;
- <CAAnimationDelegate>
- (void)animationDidStop:(CAKeyframeAnimation *)anim finished:(BOOL)flag {
/*
* 这里的key值是add时候赋予的
* [shapeLayer addAnimation:anim forKey:@"eoc"];
*/
NSLog(@"animation %@", [anim valueForKey:@"eoc"]);
//深拷贝 下面不会执行,因为地址不同
if ([anim isEqual:[shapeLayer animationForKey:@"eoc"]]) {
NSLog(@"111");
}
}
4. 组动画
- 值得注意的是,如果要设置多个动画的相对的进行时间,我们只需要在 animationGroup.beginTime 使用一次 CACurrentMediaTime() 属性,在 anim1 或者 anim2 中的 beginTime 属性里不需要使用 CACurrentMediaTime(),直接赋一个值就好,比如
anim1.beginTime = 2.f;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = @[anim1, anim2];
animationGroup.beginTime = CACurrentMediaTime()+1;
animationGroup.duration = 4.f;
animationGroup.repeatCount = MAXFLOAT;
[shapeLayer addAnimation:animationGroup forKey:nil];
5. CAMediaTiming
5.1 speed属性
- speed 流速,可以理解为动画的速度
- 默认值为1.f,如果设置为 0,layer上的动画不会执行;设置为 2,动画 2倍数进行
5.2 基于 speed = 0;
的可控进度的交互动画
- 通过滑块可以自己控制飞机飞行的位置
shapeLayer = [CAShapeLayer layer];
shapeLayer.contents = (__bridge id)[UIImage imageNamed:@"plane.png"].CGImage;
shapeLayer.frame = CGRectMake(25.f, 75.f, 50.f, 50.f);
shapeLayer.speed = 0.f; // Laye 的流速影响 anim 的流速
[self.view.layer addSublayer:shapeLayer];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position"];
anim.delegate = self;
anim.duration = 10.f;
anim.fromValue = [NSValue valueWithCGPoint:CGPointMake(50.f, 50.f)];
anim.toValue = [NSValue valueWithCGPoint:CGPointMake(300.f, 300.f)];
[shapeLayer addAnimation:anim forKey:nil];
UISlider *slider = [[UISlider alloc] initWithFrame:CGRectMake(100.f, 300.f, 200.f, 30.f)];
[slider addTarget:self action:@selector(sliderAction:) forControlEvents:UIControlEventValueChanged];
slider.minimumValue = 0;
slider.maximumValue = 10.f;
[self.view addSubview:slider];
- (void)sliderAction:(UISlider *)slider {
//这里是Layer.timeOffset
shapeLayer.timeOffset = slider.value;
}
5.3 CA动画的播放与暂停
- 一个用 touch 实现的播放与暂停
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (0 == i) { //暂停
/*
* timeoffset 和 时间相关的流逝 没什么太大的关系
* timeoffset 类型一个进度条, 你在晚上播放和 早上播放 都在在进度条暂停的那个位置
* 这个主要是把,当前这个时间点,转化为进度条上的某个时间点
*/
pausedTime = [shapeLayer convertTime:CACurrentMediaTime() fromLayer:nil];
shapeLayer.speed = 0.f;
shapeLayer.timeOffset = pausedTime;
} else if (1 == i) { //开始
shapeLayer.speed = 1.f;
shapeLayer.beginTime = CACurrentMediaTime();
}
i++;
i%=2;
}
6. 转场动画
并不作用于指定的图层属性(比如 keypath 为 bounds position),这就是说你可以在即使不能准确得 知改变了什么的情况下对图层做动画
*/
//合法的转场动画类型有:
//fade:默认。faker淡出,layer淡入
//moveIn:layer移入覆盖faker
//push:layer推入,faker推出
//reveal:覆盖在layer上面的faker被移出
//私有:(被苹果ban了,不建议直接使用)
//cube:立方体旋转,layer将会在呈现的面,faker在不可见的面
//suckEffect:覆盖在layer上面的faker被抽离
//oglFlip:将背面的layer翻转到前面,faker翻转到背面、、
//rippleEffect:伴随着水面波动动画,faker淡出,layer淡入
//pageCurl:翻到下一页,faker被翻走,呈现layer
//pageUnCurl:翻回上一页,layer被翻回并覆盖faker
//cameraIrisHollowOpen:下面这两个是特殊的。镜头开,同时呈现部分为透明,而不是layer
//cameraIrisHollowClose:类似上面,镜头关
//subtype
//4个子类型,表示上左下右4个转场动画方向:
//fromTop
//fromLeft
//fromBottom
//fromRight
- 示例动画代码
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
shapeLayer = [CAShapeLayer layer];
shapeLayer.contents = (__bridge id)[UIImage imageNamed:@"plane.png"].CGImage;
shapeLayer.frame = CGRectMake(25.f, 75.f, 50.f, 50.f);
[self.view.layer addSublayer:shapeLayer];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CATransition *anim = [CATransition animation];
anim.type = @"cube";
anim.subtype = kCATransitionFromRight;
anim.duration = 4.f;
[shapeLayer addAnimation:anim forKey:nil];
shapeLayer.contents = (__bridge id)[UIImage imageNamed:@"bg-mine.png"].CGImage;
}
- 效果