iOS开发-核心动画(CAAnimation)相关
此文章单方面对 贝塞尔曲线画图 核心动画 图层相关 方面做下整理,方便查看!
基本概念
Core Animation(核心动画)是一组功能强大,在开发中可以用他来实现很多复杂和绚丽的动画效果,核心动画作用在CALayer(Core animation layer)上
结构
盗图 -_-.png代码和效果
/**
* fillMode 视图在非Active时的行为
*
* kCAFillModeForwards 动画开始之后layer迅速移到动画开始的位置
* kCAFillModeBackwards 动画被添加的那一刻(动画开始之前)layer迅速移到开始位置,并且在 removedOnCompletion 为 NO 的情况下,动画结束会移到layer本身的位置
* kCAFillModeBoth 动画添加的那一刻(动画开始之前)layer迅速移到开始位置,并且在 removedOnCompletion 为 NO 的情况下,动画结束layer会停留在动画结束的位置
* kCAFillModeRemoved 动画开始之后layer迅速移到动画开始的位置,并且在 removedOnCompletion 为 NO 的情况下,动画结束回忆道layer本身位置
*/
/**
* timingFunction 动画节奏 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
*
* kCAMediaTimingFunctionLinear 匀速
* kCAMediaTimingFunctionEaseIn 慢进
* kCAMediaTimingFunctionEaseOut 慢出
* kCAMediaTimingFunctionEaseInEaseOut 慢进慢出
* kCAMediaTimingFunctionDefault 默认值(慢进慢出)
*/
/**
* removedOnCompletion 动画执行完毕后是否从图层上移除 默认为 YES (动画结束layer移到本身位置)
*/
/**
* 这样个要配合使用 repeatDuration = repeatCount * duration(动画一遍持续时间,主要控制速度)
* repeatCount 动画重复执行次数
* repeatDuration 动画重复执行时间
*/
CABasicAnimation
1.位置相关动画 (position.y 和 position.x transform.translation.x 和 transform.translation.y)
position fromValue:默认自身位置为起始位置 toValue:移动后的位置
transform.translation fromValue:默认自身位置为起始位置(默认0) toValue:相对于layer原始位置距离
- position.y 和 position.x
/// y轴方向移动 fromValue:默认自身位置为起始位置 toValue:移动后的位置
- (void)animation_CABasicAnimation_position_y
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position.y"];
//animation.fromValue = @(self.img.center.y); 默认初始位置
animation.toValue = @(self.img.center.y + 100);
animation.duration = 3;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.img.layer addAnimation:animation forKey:@"position.y"];
}
position.x/y.gif
- transform.translation.x 和 transform.translation.y
- (void)animation_CABasicAnimation_translation_x
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
//animation.fromValue = @0; 默认初始位置
animation.toValue = @100;
animation.duration = 1;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.img.layer addAnimation:animation forKey:@"transform.translation.x"];
}
transform.translation.x/y.gif
2.旋转相关动画
- transform.rotation.x | transform.rotation.y | transform.rotation.z
3.缩放相关动画
- transform.scale.x | transform.scale.y | transform.scale.z
- (void)animation_CABasicAnimation_transform_scale
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
animation.toValue = @8.0;
animation.duration = 3;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.img.layer addAnimation:animation forKey:@"transform.scale"];
}
transform.scale.gif
4.自身大小相关
- bounds.origin.x | bounds.origin.y
- 动画view图层上图层和view上的其他控件 不动画view本身
- 方向x 右为负 左为正
- 方向y 上为负 下为正
//bounds.origin.x
//animation1.fromValue = @0; 默认自身位置为开始位置 为0
animation1.toValue = @(-self.img.bounds.size.width * 0.5);
//bounds.origin.y
//animation2.fromValue = @0; 默认自身位置为开始位置 为0
animation2.toValue = @(-self.img.bounds.size.height * 0.5);
bounds.origin.x/y.gif
- bounds.size.width | bounds.size.height
- 动画view本身 不动画view上的其他控件
//bounds.size.width
//animation1.fromValue = @(self.img.bounds.size.width); 默认起始值为自身宽度
animation1.toValue = @(self.img.bounds.size.width * 0.5);
//bounds.size.height
//animation2.fromValue = @(self.img.bounds.size.height); 默认起始值为自身高度
animation2.toValue = @(self.img.bounds.size.height * 0.5);
bounds.size.width:height.gif
5.边角动画
- cornerRadius | borderWidth | borderColor
//cornerRadius
//animation1.fromValue = @(self.img.layer.cornerRadius); 默认自身圆角为起始值
animation1.toValue = @50;
animation1.repeatCount = 3;
animation1.repeatDuration = animation.duration * animation.repeatCount;
//borderWidth
//animation.fromValue = @(self.img.layer.borderWidth); 默认自身边框宽度为起始值
animation2.toValue = @20;
animation2.repeatCount = 3;
animation2.repeatDuration = animation.duration * animation.repeatCount;
//borderColor
//animation3.toValue = (__bridge id _Nullable)([self.img.layer borderColor]); //默认自身边框颜色为起始值
animation3.toValue = (__bridge id _Nullable)([[UIColor cyanColor] CGColor]);
animation3.repeatCount = 3;
animation3.repeatDuration = animation.duration * animation.repeatCount;
cornerRadius/borderWidth/borderColor.gif
6.自身的一些属性
- opacity 不透明
animation.fromValue = @1;
animation.toValue = @0;
opacity.gif
- backgroundColor
animation.toValue = (__bridge id _Nullable)([[UIColor yellowColor] CGColor]);
backgroundColor.gif
- contents (layer.contents)
animation.fromValue = (__bridge id _Nullable)([[UIImage imageNamed:@"春雨医生"] CGImage]);
animation.toValue = (__bridge id _Nullable)([[UIImage imageNamed:@"丁香医生"] CGImage]);
contents.gif
7.阴影相关动画
- shadowOffset 阴影位置偏移
//self.img.layer.shadowOpacity = 0.5;
//self.img.layer.shadowOffset = CGSizeMake(0, 0);
animation.fromValue = [NSValue valueWithCGSize:CGSizeMake(0, 0)];
animation.toValue = [NSValue valueWithCGSize:CGSizeMake(20, 20)];
animation.duration = 3;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
shadowOffset.gif
- shadowColor 阴影颜色
//self.img.layer.shadowOpacity = 0.5;
//self.img.layer.shadowOffset = CGSizeMake(20, 20);
animation.toValue = (__bridge id _Nullable)([UIColor redColor].CGColor);
shadowColor.gif
- shadowOpacity 阴影不透明值 01(透明完全不透明)
//self.img.layer.shadowOpacity = 0;
//self.img.layer.shadowOffset = CGSizeMake(20, 20);
animation.fromValue = @0;
animation.toValue = @1;
shadowOpacity.gif
- shadowRadius 暂且叫做阴影模糊度吧
//self.img.layer.shadowOpacity = 0.5;
//self.img.layer.shadowOffset = CGSizeMake(20, 20);
animation.fromValue = @3;
animation.toValue = @10;
shadowRadius.gif
CAKeyframeAnimation
可以看做是一个有更多位置设定的CABaseAnimation,可以设定keyPath起点、中间关键点(可以是多个)、终点的值,每一帧所对应的时间,动画会沿着设定点进行移动
CAKeyframeAnimation的一些独有属性
- values: 关键帧数组对象,里面每一个元素即为一个关键帧
- path: 动画路径对象,可以指定一个路径,在执行动画时路径会沿着路径移动,注:Path在动画中只会影响视图的Position
- keyTimes: 设置关键帧对应的时间数组,范围:0.0-1.0之间的浮点型
- 数组中的每一个连续值都必须大于或等于前面的值,因为里面存储的是动画持续时间内的每一帧的时间点,时间点是从0%-100%,时间不可能回退
- 为了得到最好的结果,数组中的元素个数应该与values中的元素个数或路径属性中的控制点的数量相匹配
- timingFunctions: 设置关键帧对应速度效果的数组
- calculationMode: 这个属性用来设定 关键帧中间的值是怎么被计算的
NSString * const kCAAnimationLinear
NSString * const kCAAnimationDiscrete 只展示关键帧的状态,没有中间过程,没有动画
NSString * const kCAAnimationPaced
NSString * const kCAAnimationCubic
NSString * const kCAAnimationCubicPaced
- (void)animation_CAKeyframeAnimation_Rect
{
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.duration = 4.0;
animation.repeatCount = 2;
animation.repeatDuration = animation.duration * animation.repeatCount;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
NSValue *value1 = [NSValue valueWithCGPoint:CGPointMake(self.img.center.x, self.img.center.y)];
NSValue *value2 = [NSValue valueWithCGPoint:CGPointMake(self.img.center.x + 150, self.img.center.y)];
NSValue *value3 = [NSValue valueWithCGPoint:CGPointMake(self.img.center.x + 150, self.img.center.y + 150)];
NSValue *value4 = [NSValue valueWithCGPoint:CGPointMake(self.img.center.x, self.img.center.y + 150)];
NSValue *value5 = [NSValue valueWithCGPoint:CGPointMake(self.img.center.x, self.img.center.y)];
animation.values = @[value1, value2, value3, value4, value5];
animation.keyTimes = @[@0, @0.4, @0.5, @0.9, @1.0];
/* 利用贝塞尔画的一个矩形,跟上面效果一样,只不过不能设置关键帧动画时间
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(self.img.center.x, self.img.center.y, 150, 150)];
animation.path = path.CGPath;
*/
[self.img.layer addAnimation:animation forKey:@"position"];
}
values_rect.gif
path_rect.gif
利用贝塞尔画个圆路径动画
- (void)animation_CAKeyframeAnimation_Circle
{
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.duration = 3;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.repeatCount = 2;
animation.repeatDuration = animation.repeatCount * animation.duration;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.img.center.x + 75, self.img.center.y) radius:75 startAngle:M_PI endAngle:3*M_PI clockwise:YES];
animation.path = path.CGPath;
[self.img.layer addAnimation:animation forKey:@"position"];
}
path_circle.gif
CATransition
转场动画,在开发中巧用会有意想不到的效果,还方便
/**
* CATransition (type) 过渡动画的类型
*
* kCATransitionFade 渐变
* kCATransitionMoveIn 覆盖
* kCATransitionPush 推出
* kCATransitionReveal 揭开(可以说是抽开)
*
* 私有动画类型的值有:"cube"、"suckEffect"、"oglFlip"、 "rippleEffect"、"pageCurl"、"pageUnCurl"等等
*/
/**
* CATransition (subtype) 过渡动画的方向
*
* kCATransitionFromRight 从右边
* kCATransitionFromLeft 从左边
* kCATransitionFromTop 从顶部
* kCATransitionFromBottom 从底部
*/
下面来组示例,看下效果,生成四个颜色的图片,来个切换动画
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int i = 0;
i = i >= self.imgsArr.count-1 ? 0:i+1;
self.img.image = [self.imgsArr objectAtIndex:i];
CATransition *animation = [CATransition animation];
animation.duration = 0.5;
animation.type = @"cube";
animation.subtype = kCATransitionFromRight;
[self.img.layer addAnimation:animation forKey:@"transition"];
}
- (NSArray *)imgsArr
{
if (!_imgsArr) {
_imgsArr = [NSArray arrayWithObjects:[self returnImage1WithCGSize:self.img.bounds.size andColor:[UIColor redColor]],
[self returnImage1WithCGSize:self.img.bounds.size andColor:[UIColor greenColor]],
[self returnImage1WithCGSize:self.img.bounds.size andColor:[UIColor cyanColor]],
[self returnImage1WithCGSize:self.img.bounds.size andColor:[UIColor magentaColor]], nil];
}
return _imgsArr;
}
//根据size和传进来的color生成一张颜色图片
- (UIImage *)returnImage1WithCGSize:(CGSize)size andColor:(UIColor *)color
{
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, size.width, size.height)];
[color setFill];
[path fill];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
我平时开发中的图片轮播一般都是用这个动画做的,加两个手势就行,缺点就是不能翻页中间不能停留,没有scroll那么全的功能,但是代码简单方便
kCATransitionPush_模仿轮播图.gif
kCATransitionFade.gif
kCATransitionMoveIn.gif
kCATransitionReveal.gif
私有-cube.gif
示例: 我最近做项目遇到这样一个问题,登录界面跳到主界面的时候我一般喜欢用根视图去跳,因为我认为登录注册界面可能八百年才用到一次,用导航或者模态来转场不是太好,但是用根视图跳转显得太突兀,动画不太好看,这时转场动画便派上用场
//登录
SideslipViewController *sideslip = [[SideslipViewController alloc] init];
[UIApplication sharedApplication].keyWindow.rootViewController = sideslip;
CATransition *transition = [[CATransition alloc] init];
transition.duration = 0.3;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
transition.type = kCATransitionReveal;
transition.subtype = kCATransitionFromTop;
[[UIApplication sharedApplication].keyWindow.layer addAnimation:transition forKey:@"LoginIn"];
//登出
LoginViewController *login = [[LoginViewController alloc] init];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:login];
nav.navigationBar.hidden = YES;
[UIApplication sharedApplication].keyWindow.rootViewController = nav;
CATransition *transition = [[CATransition alloc] init];
transition.duration = 0.3;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromBottom;
[[UIApplication sharedApplication].keyWindow.layer addAnimation:transition forKey:@"LoginOut"];
LoginInOrOut_Animation.gif
CASpringAnimation
iOS9之后出来的新的弹簧动画,继承CABasicAnimation
/** 一些重要的属性
* CASpringAnimation的重要属性:
*
* mass:质量(影响弹簧的惯性,质量越大,弹簧惯性越大,运动的幅度越大)
* stiffness:弹性系数(弹性系数越大,弹簧的运动越快)
* damping:阻尼系数(阻尼系数越大,弹簧的停止越快)
* initialVelocity:初始速率(弹簧动画的初始速度大小,弹簧运动的初始方向与初始速率的正负一致,若初始速率为0,表示忽略该属性)
* settlingDuration:结算时间(根据动画参数估算弹簧开始运动到停止的时间,动画设置的时间最好根据此时间来设置)
*/
CASpringAnimation *animation = [CASpringAnimation animationWithKeyPath:@"position"];
animation.mass = 10.0;
animation.stiffness = 500;
animation.damping = 10;
animation.initialVelocity = 5.0f;
animation.duration = animation.settlingDuration; //时间需要注意下,用系统计算出来动画所需时间
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(self.img.center.x, self.img.center.y + 200)];
[self.img.layer addAnimation:animation forKey:@"position"];
CASpringAnimation_position.gif
以上零散单个Demo地址: https://github.com/SupermanChao/Animation
CAAnimationGroup
使用Group可以将多个动画合并一起加入到图层中,Group中所有动画一起执行,可以展示很多动画种类
一个简单的动画组例子,选取颜色
Demo地址:https://github.com/SupermanChao/AnimationGroup
CATransaction
最后讲一下事务(CATransaction),在核心动画里面存在事务(CATransaction)这样一个概念,它负责协调多个动画原子更新显示操作,简单来说事务是核心动画里面的一个基本的单元,动画的产生必然伴随着layer的Animatable属性的变化,而layer属性的变化必须属于某一个事务
事务分为隐式和显式:
- 隐式:没有明显调用事务的方法,由系统自动生成事务,比如直接设置一个layer的position属性,则会在当前线程自动生成一个事务,并在下一个runLoop中自动commit事务
- 显式:明显调用事务的方法([CATransaction begin]和[CATransaction commit])
事务的可设置属性(会覆盖隐式动画的设置):
//动画持续时间
+ (CFTimeInterval)animationDuration;
//动画时间曲线
+ (nullable CAMediaTimingFunction *)animationTimingFunction;
//是否关闭动画
+ (BOOL)disableActions;
//动画执行完毕回调
+ (nullable void (^)(void))completionBlock;
下面来个小例子,圆环进度动画,先看效果后上代码
代码地址:https://github.com/SupermanChao/CATransaction-Demo
//圆环贝塞尔曲线
- (UIBezierPath *)path
{
if (!_path) {
CGFloat radius = (MIN(self.frame.size.width, self.frame.size.height) - self.lineWidth) * 0.5;
CGPoint center = CGPointMake(self.frame.size.width * 0.5, self.frame.size.height * 0.5);
_path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:-M_PI_2 endAngle:M_PI + M_PI_2 clockwise:YES];
}
return _path;
}
//轨道
- (CAShapeLayer *)outLayer
{
if (!_outLayer) {
_outLayer = [CAShapeLayer layer];
_outLayer.lineWidth = self.lineWidth;
_outLayer.fillColor = [UIColor clearColor].CGColor;
_outLayer.strokeColor = self.pathwayColor.CGColor;
_outLayer.path = self.path.CGPath;
}
return _outLayer;
}
//进度
- (CAShapeLayer *)progressLayer
{
if (!_progressLayer) {
_progressLayer = [CAShapeLayer layer];
_progressLayer.lineWidth = self.lineWidth;
_progressLayer.fillColor = [UIColor clearColor].CGColor;
_progressLayer.lineCap = kCALineCapRound;
_progressLayer.strokeColor = self.scheduleColor.CGColor;
_progressLayer.path = self.path.CGPath;
_progressLayer.strokeStart = 0;
_progressLayer.strokeEnd = 0.001;
}
return _progressLayer;
}
//定时器协调运作
- (void)onTimer
{
switch (self.accuracy) {
case LCAnimationAccuracyLow:
self.progress += 2;
break;
case LCAnimationAccuracyHeight:
self.progress += 0.5;
break;
case LCAnimationAccuracyVeryHeight:
self.progress += 0.1;
break;
default:
self.progress += 1;
break;
}
[CATransaction begin];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
[CATransaction setAnimationDuration:self.totalTime / self.count];
self.progressLayer.strokeEnd = self.progress / 100.0;
[CATransaction commit];
if (self.progress >= 99.99) {
[self lc_stopAnimation];
if ([self.delegate respondsToSelector:@selector(lc_animationFinishAction)]) {
[self.delegate lc_animationFinishAction];
}
};
}