00 动画架构
谈 UIKit 和 CoreAnimation 在 iOS 渲染中的角色
https://mp.weixin.qq.com/s?__biz=MzA5MTM1NTc2Ng==&mid=2458325030&idx=1&sn=b0682295df393e1f3d89c15708acc1f2&scene=21#wechat_redirect
https://mp.weixin.qq.com/s?__biz=MzA5MTM1NTc2Ng==&mid=2458325030&idx=2&sn=473e586bfde4b73d70fe0a57d66dfe28&chksm=870e1f3fb07996296305bafcc8a0801b0c57619635a3bc7a8e4b836e7fc6553e8b92879d3b78&cur_album_id=1700205981982343172&scene=21#wechat_redirect
UIKit 的官方定义:
Construct and manage a graphical, event-driven user interface for your iOS or tvOS app.
构建和管理一个图形化的,事件驱动的 User Interface。
UIKit 是构建和管理图形化界面的大型工具集合,操作的最小单位是 UIView。
UIView 在 iOS 渲染中的角色
UIView 是 UIKit 用来构建和管理界面的最小单位,主要行使以下三个职能:
- 负责某个区域内容的展示 (contents)
- 负责区域内用户交互的处理 (responder)
- 负责区域内子 UIView 的上述两项任务的管理 (subviews)
特别的,在渲染方面支持以下功能:
- 在XYZ轴上的布局
- 视觉属性支持积木式配置
- 视图层级管理和控制
- 多种 Animatable 属性以及简单动画的封装
封装度极高,性能优秀的UIKit为我们描述和控制UI元素(UIView)提供了非常便捷和齐全的API,实际需求开发中,90%的需求都可以用UIKit解决。
CoreAnimation 框架在 iOS 渲染中的角色
CoreAnimation 直译是动画相关的框架,实际上在 dyld_shared_cache
中,CoreAnimation 框架依附于 QuartzCore.framework
之下,QuartzCore 框架是 macOS 和 iOS 共用的 UI 图形化框架。
而当中,CALayer 是 CoreAnimation 管理的最小单位。
CALayer 是 UIView 完成第一个职能,即某个区域内容的展示 (Contents)的载体。CALayer 致力于把 CALayer.contents
定义的数据快速准确的展现在屏幕指定的区域。
CALayer 帮助我们避免使用 OpenGL ES/Metal 等低级 API 直接操作 GPU 完成绘制工作。
01 UIView 和 CALayer
iOS和MacOS同为Apple的产品,QuartzCore是跨iOS和macOS平台的2D绘制框架,为了统一View的展示逻辑同时兼顾两个系统的差异,Apple抽出了一层CALayer对象,专门用于图像展示。
以iOS为例:
// UIVIew
@interface UIView : UIResponder
@end
//CALayer
@interface CALayer : NSObject
@end
UIView继承自UIResponder,拥有了响应事件的能力,同时UIView又实现了CALayerDelegate,可以提供图层的内容,处理子图层的布局以及提供要执行的自定义动画动作.
综上可知,CALayer负责内容的展示,UIView负责事件的处理。UIView是CALayer的代理,与视图展示相关的逻辑都是由其内部的CALayer来处理的。
CALayer的模型层与展示层
在CALayer内部由两个layer:presentationLayer(以下简称P)和modelLayer(以下简称M)。presentationLayer负责走路(绘制内容),而modelLayer负责看路(如何绘制)。
P有这样的特点:
1、我们看到的一切,都是P的内容;
2、P只在下次屏幕刷新时才会进行绘制。
M有这样的特点:
1、我们我们对CALayer的各种绘图属性进行赋值和访问实际上都是访问的M的属性,比如bounds、backgroundColor、position等;
2、对这些属性进行赋值,不会影响P,也就是不会影响绘制内容。
可以把M理解成一个隐身的家伙,只有P才能感知它的存在。
当一个CAAnimation(以下称为A)加到了layer上面后,A就把M从P身上挤下去了。现在P持有的是A,P同样在每次屏幕刷新的时候去问持有的对象,A就指挥它从fromValue到toValue来改变值。而动画结束后,A会自动被移除,这时P会继续向M读取数值,此时M还处于原来的状态,于是P也就回到了原来的位置。这就是为什么动画结束后视图又回到了原来的位置,是因为我们看到在移动的是P,而指挥它移动的是A,M永远停在原来的位置没有动,动画结束后A被移除,P就回到了M的怀里。
动画结束后,P会回到M的状态(当然这是有前提的,因为动画已经被移除了,我们可以设置fillMode来继续影响P),但是这通常都不是我们动画想要的效果。我们通常想要的是,动画结束后,视图就停在结束的地方,并且此时我去访问该视图的属性(也就是M的属性),也应该就是当前看到的那个样子。按照官方文档的描述,我们的CAAnimation动画都可以通过设置modelLayer到动画结束的状态来实现P和M的同步。
CABasicAnimation* animation = [CABasicAnimation new];
animation.keyPath = @"transform.rotation";
animation.duration = 2;
animation.toValue = M_PI_2;
animation.fillMode = kCAFillModeForwards;
animation.isRemovedOnCompletion = NO;
[self.btn.layer addAnimation:animation forKey:@""];
CALayer的子类
CAShapeLayer,用来根据路径绘制矢量图形
CATextLayer,绘制文字信息
CATransformLayer,使用单独的图层创建3D图形
CAGradientLayer,绘制线性渐变色
CAReplicatorLayer,高效地创建多个相似的图层并施加相似的效果或动画
CAScrollLayer,没有交互效果的滚动图层,没有滚动边界,可以任意滚动上面的图层内容
CATiledLayer,将大图裁剪成多个小图以提高内存和性能
CAEmitterLayer,各种炫酷的粒子效果
CAEAGLLayer,用来显示任意的OpenGL图形
AVPlayerLayer,用来播放视频
UIView动画
UIView的内容显示是由CALayer负责的,UIView的动画其实就是CALayer的动画。
CALayer可以做动画 Animatable
的属性一般有:
1、位置属性:frame bounds center
2、颜色透明度属性:backgroundColor alpha
3、layer属性:圆角 边框色 阴影
4、transform属性:缩放CGAffineTransformScale、 旋转CGAffineTransformRotate(弧度制)、 位移CGAffineTransformTranslate
注:具体详见
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AnimatableProperties/AnimatableProperties.html#//apple_ref/doc/uid/TP40004514-CH11-SW2
https://blog.csdn.net/qq_42792413/article/details/86506479
如果一个属性被标记为Animatable,那么它具有以下两个特点:
1、直接对它赋值可能产生隐式动画;
2、我们的CAAnimation的keyPath可以设置为这个属性的名字。
开发中一般使用如下API:
// UIView(UIViewAnimationWithBlocks)
[UIView animateWithDuration:2 animations:^{
//具体的属性修改代码
}];
[UIView animateWithDuration:2 animations:^{
//具体的属性修改代码
} completion:^(BOOL finished) {
}];
[UIView animateWithDuration:2 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
} completion:^(BOOL finished) {
}];
// UIView (DeprecatedAnimations) ios13 后废弃,推荐使用上面的block形式。
[UIView beginAnimations:nil context:nil];
//具体的属性修改代码
[UIView setAnimationDuration:2];
[UIView setAnimationDelegate:self];
[UIView setAnimationWillStartSelector:<#(nullable SEL)#>];
[UIView setAnimationDidStopSelector:<#(nullable SEL)#>];
[UIView commitAnimations];
//transform
[UIView animateWithDuration:2 animations:^{
CGAffineTransform affineTransform = CGAffineTransformTranslate(view.transform, 100, 288);
affineTransform = CGAffineTransformScale(affineTransform, 0.7, 2);
affineTransform = CGAffineTransformRotate(affineTransform, M_PI_2);
view.transform = affineTransform;
}];
显式动画和隐式动画
https://www.jianshu.com/p/90415eb764bf
动画要分两部分考虑:怎么动?动多久? 即动画行为和动画时间两部分。CoreAnimation中表示行为的有CAAction协议,表示时间的有CAMediaTiming协议,当然CAAnimation都实现了这两个协议。
怎么动: 默认情况下,CALayer的可动画属性都关联到一个行为对象(实现了CAAction),即当直接修改CALayer的可动画属性时,会执行对应的行为对象。
动多久:CALayer也实现了CAMediaTiming协议,自身可以控制行为时间。在每一次Runloop中,都会创建隐式的事务(CATransaction),所有CALayer的属性修改都会包含到这个事务中去,而事务中CALayer的行为时间被默认设置为0.25s。 所以修改CALayer属性所触发的行为都会执行0.25s。
由上可知,UIView是CALayer的代理,当我们直接对可动画属性赋值的时候,由于有隐式动画存在的可能,CALayer首先会判断此时有没有隐式动画被触发。它会让它的delegate(没错CALayer拥有一个属性叫做delegate)调用actionForLayer:forKey:来获取一个返回值,这个返回值在声明的时候是一个id对象,当然在运行时它可能是任何对象。这时CALayer拿到返回值,将进行判断:如果返回的对象是一个nil,则进行默认的隐式动画;如果返回的对象是一个[NSNull null] ,则CALayer不会做任何动画;如果是一个正确的实现了CAAction协议的对象,则CALayer用这个对象来生成一个CAAnimation,并加到自己身上进行动画。伪代码示例如下:
- (void)setPosition:(CGPoint)position {
// [super setPosition:position];
if ([self.delegate respondsToSelector:@selector(actionForLayer:forKey:)]) {
id obj = [self.delegate actionForLayer:self forKey:@"position"];
if (!obj) {
// 隐式动画
} else if ([obj isKindOfClass:[NSNull class]]) {
// 直接重绘(无动画)
} else {
// 使用obj生成CAAnimation
CAAnimation * animation;
[self addAnimation:animation forKey:nil];
}
}
}
验证:
CALayer* layer = [CALayer layer];
[self.view.layer addSublayer:layer];
NSLog(@"%@",[layer.delegate actionForLayer:layer forKey:@"position"]);
NSLog(@"%@",[self.view.layer.delegate actionForLayer:self.view.layer forKey:@"position"]);
[UIView animateWithDuration:0.2 animations:^{
NSLog(@"%@",[self.view.layer.delegate actionForLayer:self.view.layer forKey:@"position"]);
}];
// 结果
2021-01-23 11:06:06.640786+0800 CALayerDemo[1477:39319] (null) ====> nil
2021-01-23 11:06:06.640924+0800 CALayerDemo[1477:39319] =====> NSNull
2021-01-23 11:06:06.641228+0800 CALayerDemo[1477:39319] <_UIViewAdditiveAnimationAction: 0x60000244b360>
修改layer同时不希望触发隐式动画:
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[aLayer removeFromSuperlayer];
[CATransaction commit];
CoreAnimation
CoreAnimation为iOS核心动画,来自Quartz.framework,提供了一组API用于实现一些炫酷的动画效果。动画直接作用在Layer上,而非UIView,并且动画的执行过程在后台,不阻塞主线程。
一个几何在屏幕上的位置移动动画,本质是什么?
本质是在时间的起点和终点的过程里,每一次屏幕刷新,某个物体的位置做一点点均匀的移动,人眼就会认为它在均匀的移动。
CAMediaTiming:定义了Animation的一些共有属性,时间的控制。
https://blog.csdn.net/u013282174/article/details/51605403
@protocol CAMediaTiming
/* The begin time of the object, in relation to its parent object, if
* applicable. Defaults to 0. */
@property CFTimeInterval beginTime;
/* The basic duration of the object. Defaults to 0. */
@property CFTimeInterval duration;
//........省略.........
@property(copy) CAMediaTimingFillMode fillMode;
@end
CAAnimation:动画基类
CAPropertyAnimation:属性动画,包含CABasicAnimation
和CAKeyFrameAnimation
CAAnimationGroup:组合动画
CATransition:转场动画,一般用于ViewController或者View之间的切换
CABasicAnimation:基础动画
CAKeyFrameAnimation:关键帧动画
CoreAnimation示例:
CABasicAnimation:
动画有三个重要的属性:起始位置
、终止位置
、持续时间
。
CABasicAnimation主要的作用就是在要做动画的属性上,在始末位置之间插值,这样View就会在持续时间内在始末位置之间有个平滑的过度。
CABasicAnimation* animation = [CABasicAnimation new];
animation.keyPath = @"transform.rotation";
animation.duration = 2;
animation.toValue = [NSNumber numberWithDouble:M_PI_2];
// animation.fillMode = kCAFillModeForwards;
// animation.isRemovedOnCompletion = NO;
[self.btn.layer addAnimation:animation forKey:@""];
CAKeyFrameAnimation
CAKeyFrameAnimation的思想也是插值,它是在每个帧之间插值来实现过度。
值动画:
let view:UIView = UIView()
view.backgroundColor = UIColor.red
view.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
self.view.addSubview(view)
let animation:CAKeyframeAnimation = CAKeyframeAnimation()
animation.duration = 3.0
animation.keyPath = "opacity"
let valuesArray:[NSNumber] = [NSNumber(value: 0.95 as Float),
NSNumber(value: 0.90 as Float),
NSNumber(value: 0.88 as Float),
NSNumber(value: 0.85 as Float),
NSNumber(value: 0.35 as Float),
NSNumber(value: 0.05 as Float),
NSNumber(value: 0.0 as Float)]
animation.values = valuesArray
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
view.layer.add(animation, forKey: nil)
路径动画:
imageView.frame = CGRect(x:50,y:50,width:50,height:50)
imageView.image = UIImage(named: "Plane.png")
self.view.addSubview(imageView)
let pathLine:CGMutablePath = CGMutablePath()
pathLine.move(to: CGPoint(x:50,y:50))
pathLine.addLine(to: CGPoint(x:200,y:150))
pathLine.addLine(to: CGPoint(x:300,y:50))
let animation:CAKeyframeAnimation = CAKeyframeAnimation()
animation.duration = 2.0
animation.path = pathLine
animation.keyPath = "position"
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
imageView.layer.add(animation, forKey: nil)
CAAnimationGroup
let animationGroup:CAAnimationGroup = CAAnimationGroup()
animationGroup.animations = [rotate,scale,move]
animationGroup.duration = 2.0
animationGroup.fillMode = CAMediaTimingFillMode.forwards;
animationGroup.isRemovedOnCompletion = false
loginButton?.layer.add(animationGroup, forKey:nil)
转场动画
1、layer之间的切换
let transition = CATransition()
/// 动画时长
transition.duration = CFTimeInterval(duration)
/// 动画类型
transition.type = CATransitionType(rawValue: "cameraIrisHollowOpen") || CATransitionType.moveIn
/// 动画方向
transition.subtype = animationSubType(subType: subType)
/// 缓动函数
transition.timingFunction = CAMediaTimingFunction(name: animationCurve(curve: curve))
/// 完成动画删除
transition.isRemovedOnCompletion = true
layer.add(transition,forKey: key)
2.vc之间的切换
系统自带实现
,仅限同级childViewController
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion API_AVAILABLE(ios(5.0));
typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
UIViewAnimationOptionLayoutSubviews = 1 << 0,
UIViewAnimationOptionAllowUserInteraction = 1 << 1, // turn on user interaction while animating
UIViewAnimationOptionBeginFromCurrentState = 1 << 2, // start all views from current value, not initial value
UIViewAnimationOptionRepeat = 1 << 3, // repeat animation indefinitely
UIViewAnimationOptionAutoreverse = 1 << 4, // if repeat, run animation back and forth
UIViewAnimationOptionOverrideInheritedDuration = 1 << 5, // ignore nested duration
UIViewAnimationOptionOverrideInheritedCurve = 1 << 6, // ignore nested curve
UIViewAnimationOptionAllowAnimatedContent = 1 << 7, // animate contents (applies to transitions only)
UIViewAnimationOptionShowHideTransitionViews = 1 << 8, // flip to/from hidden state instead of adding/removing
UIViewAnimationOptionOverrideInheritedOptions = 1 << 9, // do not inherit any options or animation type
UIViewAnimationOptionCurveEaseInOut = 0 << 16, // default
UIViewAnimationOptionCurveEaseIn = 1 << 16,
UIViewAnimationOptionCurveEaseOut = 2 << 16,
UIViewAnimationOptionCurveLinear = 3 << 16,
UIViewAnimationOptionTransitionNone = 0 << 20, // default
UIViewAnimationOptionTransitionFlipFromLeft = 1 << 20,
UIViewAnimationOptionTransitionFlipFromRight = 2 << 20,
UIViewAnimationOptionTransitionCurlUp = 3 << 20,
UIViewAnimationOptionTransitionCurlDown = 4 << 20,
UIViewAnimationOptionTransitionCrossDissolve = 5 << 20,
UIViewAnimationOptionTransitionFlipFromTop = 6 << 20,
UIViewAnimationOptionTransitionFlipFromBottom = 7 << 20,
UIViewAnimationOptionPreferredFramesPerSecondDefault = 0 << 24,
UIViewAnimationOptionPreferredFramesPerSecond60 = 3 << 24,
UIViewAnimationOptionPreferredFramesPerSecond30 = 7 << 24,
} API_AVAILABLE(ios(4.0));
自定义实现
UIViewControllerContextTransitioning
:转场动画回调的上下文对象
@protocol UIViewControllerContextTransitioning
@property(nonatomic, readonly) UIView *containerView; //容器View
@property(nonatomic, readonly, getter=isAnimated) BOOL animated;
@property(nonatomic, readonly, getter=isInteractive) BOOL interactive;
@property(nonatomic, readonly) BOOL transitionWasCancelled;
@property(nonatomic, readonly) UIModalPresentationStyle presentationStyle;
@property(nonatomic, readonly) CGAffineTransform targetTransform API_AVAILABLE(ios(8.0));
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)finishInteractiveTransition;
- (void)cancelInteractiveTransition;
- (void)pauseInteractiveTransition API_AVAILABLE(ios(10.0));
- (void)completeTransition:(BOOL)didComplete;
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key API_AVAILABLE(ios(8.0));
- (CGRect)initialFrameForViewController:(UIViewController *)vc;
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
@end
UIViewControllerTransitioningDelegate
:ViewController
@protocol UIViewControllerTransitioningDelegate
@optional
// 获取Transition Animator对象
- (nullable id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed;
// 获取交互式动画器对象
- (nullable id )interactionControllerForPresentation:(id )animator;
- (nullable id )interactionControllerForDismissal:(id )animator;
// 获取自定义演示控制器
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source API_AVAILABLE(ios(8.0));
@end
UINavigationControllerDelegate
:NavigationController代理
@protocol UINavigationControllerDelegate
@optional
// 响应显示的视图控制器
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
// 管理方向旋转
- (UIInterfaceOrientationMask)navigationControllerSupportedInterfaceOrientations:(UINavigationController *)navigationController API_AVAILABLE(ios(7.0)) API_UNAVAILABLE(tvos);
- (UIInterfaceOrientation)navigationControllerPreferredInterfaceOrientationForPresentation:(UINavigationController *)navigationController API_AVAILABLE(ios(7.0)) API_UNAVAILABLE(tvos);
// 返回处理push/pop手势过渡的对象 这个代理方法依赖于上方的方法,这个代理实际上是根据交互百分比来控制上方的动画过程百分比
- (nullable id )navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id ) animationController API_AVAILABLE(ios(7.0));
// 返回处理push/pop动画过渡的对象
- (nullable id )navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC API_AVAILABLE(ios(7.0));
@end
动画代理
UIViewControllerAnimatedTransitioning:简单动画
@protocol UIViewControllerAnimatedTransitioning
- (NSTimeInterval)transitionDuration:(nullable id )transitionContext;
- (void)animateTransition:(id )transitionContext;
@optional
- (id ) interruptibleAnimatorForTransition:(id )transitionContext API_AVAILABLE(ios(10.0));
- (void)animationEnded:(BOOL) transitionCompleted;
@end
UIViewControllerInteractiveTransitioning:交互式
@protocol UIViewControllerInteractiveTransitioning
- (void)startInteractiveTransition:(id )transitionContext;
@optional
@property(nonatomic, readonly) CGFloat completionSpeed;
@property(nonatomic, readonly) UIViewAnimationCurve completionCurve;
@property (nonatomic, readonly) BOOL wantsInteractiveStart API_AVAILABLE(ios(10.0));
@end
UIPercentDrivenInteractiveTransition:百分比
UIKIT_EXTERN API_AVAILABLE(ios(7.0)) @interface UIPercentDrivenInteractiveTransition : NSObject
@property (readonly) CGFloat duration;
@property (readonly) CGFloat percentComplete;
@property (nonatomic,assign) CGFloat completionSpeed;
@property (nonatomic,assign) UIViewAnimationCurve completionCurve;
@property (nullable, nonatomic, strong)id timingCurve API_AVAILABLE(ios(10.0));
@property (nonatomic) BOOL wantsInteractiveStart API_AVAILABLE(ios(10.0));
- (void)pauseInteractiveTransition API_AVAILABLE(ios(10.0));
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;
@end
一些与动画有关的工具
1、CADisplayLink
频率能达到屏幕刷新率的定时器类,可以避免NSTimer的误差。
2、向量
iOS的屏幕就是二维直角坐标系,动画中的坐标涉及到大量的向量运算。
3、贝塞尔曲线 Bézier curve
贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般通过它来精确画出曲线。贝塞尔曲线完全由其控制点决定其形状, n个控制点对应着n-1阶的贝塞尔曲线,并且可以通过递归的方式来绘制。
怎么理解贝塞尔曲线? - FrancisZhao的回答 - 知乎 https://www.zhihu.com/question/29565629/answer/1184466425
示例代码
https://wos2.58cdn.com.cn/DeFazYxWvDti/frsupload/dd922d8e5a9eed071e2beb6e0031f213.zip