iOS开发中的动画

00 动画架构

image

谈 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:属性动画,包含CABasicAnimationCAKeyFrameAnimation

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

你可能感兴趣的:(iOS开发中的动画)