美文网首页
IOS基础:动画

IOS基础:动画

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-20 10:08 被阅读0次

    原创:知识点总结性文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 比较
    • 一、UIView 动画
      • 1、简介
      • 2、演示
    • 二、Core Animation
      • 1、简介
      • 2、CABasicAnimation
      • 3、CAKeyframeAnimation
      • 4、CATransition
      • 5、CASpringAnimation
      • 6、CAAnimationGroup
      • 7、CADisplayLink
    • Demo
    • 参考文献

    比较

    iOS系统本身提供了两套绘图的框架,即UIBezierPathCore Graphics。前者所属UIKit,其实是对Core Graphics框架关于path的进一步封装,所以使用起来比较简单。但是毕竟Core Graphics更接近底层,所以它更加强大。我们使用UIView的 -(void)drawRect:方法绘图就是使用的CoreGraphics框架。

    CoreGraphics(核心图形)

    它是iOS的核心图形库,包含Quartz2D绘图API接口,常用的CGFloatCGSizeCGPoint等这些图形,都定义在这个框架中,类名以CG开头的都属于CoreGraphics框架,它提供的都是C语言函数接口,是可以在iOSmac OS通用的。

    CoreAnimation(核心动画)

    用来做动画的,非常的简单,但是效果非常绚丽。

    • CoreAnimation是跨平台的,既可以支持IOS,也支持MAC OS
    • CoreAnimation执行动画是在后台,不会阻塞主线程。
    • CoreAnimation作用在CALayer,不是UIView
    CoreGraphics和CoreAnimation的关系

    它们都是跨iOSMac OS使用的,这点区别于UIKit,并且- CoreAnimation中大量使用到CoreGraphics中的类,因为实现动画要用到图形库中的东西。


    一、UIView 动画

    1、简介

    比较
    • 简单的动画不会直接使用Core Animation框架,而是使用UIView动画,本质上也是Core Animation框架实现的,只是进行了封装和优化
    • 每个视图都关联到一个图层(CALayer)对象,视图主要用来处理事件,图层用来处理动画
    • 视图有一系列支持动画的属性,如frameboundscenteralphatransform等,其他如动画延迟事件、动画曲线(-in缓入 out缓出 linear线性匀速 )、动画过渡、重复次数、自动反转(FlipFromRight从右往左翻转 FlipFromBottom从下往上翻转CurlUp向上翻页 CurlDown向下翻页)
    • options选项类型中成员值是位掩码,位或运算结果时两种效果叠加,如options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationTransitionFlipFromRight即过渡动画以缓出速度从左往右翻转
    常用枚举类型
    视图动画曲线 UIViewAnimationCurve
    视图内容填充模式 UIViewContentMode
    视图动画过渡效果 UIViewAnimationTransition
    视图自动调整大小方式 UIViewAutoresizing
    视图的动画选项 UIViewAnimationOptions
    视图关键帧动画选项 UIViewKeyframeAnimationOptions
    视图的系统动画 UISystemAnimation
    布局约束的轴 UILayoutConstraintAxis(水平还是竖直)
    
    常见视图动画扩展
    UIViewGeometry:视图几何相关的扩展 
    UIViewHierarchy:视图层次结构相关的扩展
    UIViewRendering:视图外观渲染相关的扩展,例如是否隐藏、透明度、背景颜色等
    UIViewAnimation:视图动画相关的扩展
    UIViewAnimationWithBlocks:视图用block快速定义动画的扩展
    UIViewKeyframeAnimations:视图关键帧动画相关的扩展
    UIViewGestureRecognizers:视图上手势相关的扩展 
    UIViewMotionEffects:视图上运动效果相关的扩展
    UIConstraintBasedLayoutCoreMethods:视图上约束相关的扩展
    UISnapshotting:视图快照相关的扩展
    

    2、动画演示

    动画演示 动画演示 动画演示 动画演示
    - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
        // 红方块起始frame
        if (CGRectEqualToRect(self.redOriginFrame, CGRectZero))
        {
            self.redOriginFrame = self.redView.frame;
        }
        
        // 蓝方块起始frame
        if (CGRectEqualToRect(self.blueOriginFrame, CGRectZero))
        {
            self.blueOriginFrame = self.blueView.frame;
        }
        
        
        switch (indexPath.item)
        {
            case 0:// 重置
            {
                self.redView.frame = self.redOriginFrame;// 红方块起始frame
                self.redView.alpha = 1.0;// 透明
                self.redView.transform = CGAffineTransformIdentity;// 移动
                self.redView.backgroundColor = [UIColor redColor];
                
                self.blueView.frame = self.blueOriginFrame;// 蓝方块起始frame
                
                break;
            }
            case 1:// 移动红色方块
            {
                CGRect redFrame = self.redView.frame;
                
                // 下移100点
                redFrame.origin.y += 100;
                // 产生动画效果
                [UIView animateWithDuration:UIAnimationDuration animations:^{
                    // 红方块新的frame
                    self.redView.frame = redFrame;
                }];
                break;
            }
            case 2:// 缩小红色方块,蛮奇怪的,它放大以后再缩小,我明明是减小宽高
            {
                CGRect redFrame = self.redView.frame;
                
                // 缩小宽高
                redFrame.size.width -= 50;
                redFrame.size.height -= 50;
                // 产生动画效果
                [UIView animateWithDuration:UIAnimationDuration animations:^{
                    // 红方块新的frame
                    self.redView.frame = redFrame;
                }];
                break;
            }
            case 3:// 旋转红色方块
            {
                // 获取初始transform属性
                CGAffineTransform transform = self.redView.transform;
                // 旋转90度
                transform = CGAffineTransformRotate(transform, M_PI/4);
                // 产生动画效果
                [UIView animateWithDuration:UIAnimationDuration animations:^{
                    // 红方块新的transform属性
                    self.redView.transform = transform;
                }];
                break;
            }
            case 4:// 改变红色为紫色
            {
                [UIView animateWithDuration:UIAnimationDuration animations:^{
                    self.redView.backgroundColor = [UIColor purpleColor];
                }];
                break;
            }
            case 5:// 改变透明度为半透明
            {
                [UIView animateWithDuration:UIAnimationDuration animations:^{
                    self.redView.alpha = 0.5;
                }];
                break;
            }
            case 6:// 移动红色方块并同时旋转90度
            {
                // 下移
                CGRect redFrame = self.redView.frame;
                redFrame.origin.y += 100;
                
                // 旋转
                CGAffineTransform transform = self.redView.transform;
                transform = CGAffineTransformRotate(transform, M_PI/2);
                
                // 产生动画效果
                [UIView animateWithDuration:UIAnimationDuration animations:^{
                    // 新的transform和frame属性
                    self.redView.frame = redFrame;
                    self.redView.transform = transform;
                }];
                break;
            }
            case 7:// 先移动后旋转
            {
                // 下移
                CGRect redFrame = self.redView.frame;
                redFrame.origin.y += 100;
                
                // 旋转
                CGAffineTransform transform = self.redView.transform;
                transform = CGAffineTransformRotate(transform, M_PI/2);
                
                [UIView animateWithDuration:UIAnimationDuration animations:^{// 下移动画
                    self.redView.frame = redFrame;
                } completion:^(BOOL finished) {// 完成后进入旋转动画
                    [UIView animateWithDuration:UIAnimationDuration animations:^{
                        self.redView.transform = transform;
                    } completion:^(BOOL finished) {// 完成后输出旋转完成
                        NSLog(@"旋转完成");
                    }];
                }];
                break;
            }
            case 8:// 通过中心下移
            {
                CGPoint center = self.redView.center;
                // 下移100点
                center.y += 100;
                [UIView animateWithDuration:UIAnimationDuration animations:^{
                    // 新的中心
                    self.redView.center = center;
                }];
                break;
            }
            case 9:// 渐变方式进行缩放
            {
                // 缩放一半
                CGAffineTransform transform = self.redView.transform;
                transform = CGAffineTransformScale(transform, 0.5, 0.5);
                // 产生动画效果
                [UIView animateWithDuration:UIAnimationDuration delay:1.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
                    // 新的transform属性
                    self.redView.transform = transform;
                } completion:^(BOOL finished) {
                    NSLog(@"缩放完成");
                }];
                break;
            }
            case 10:// 通过中心让蓝色和红色方块同时右移
            {
                CGPoint redCenter = self.redView.center;
                CGPoint blueCenter = self.blueView.center;
                
                // 下移100点
                redCenter.x += 100;
                blueCenter.x += 100;
                
                // 产生动画效果
                [UIView animateWithDuration:UIAnimationDuration animations:^{
                    self.redView.center = redCenter;
                    self.blueView.center = blueCenter;
                }];
                break;
            }
            case 11:// 重复来回移动
            {
                CGPoint center = self.redView.center;
                
                // 下移100点
                center.y += 100;
                // 重复/自动逆向
                [UIView animateWithDuration:UIAnimationDuration delay:0 options:UIViewAnimationOptionRepeat|UIViewAnimationOptionAutoreverse animations:^{
                    self.redView.center = center;
                } completion:nil];
                break;
            }
            default:
                break;
        }
    }
    

    二、Core Animation

    1、简介

    a、Core Animation
    区别

    UIView动画可以看成是对Core Animation的封装,和UIView动画不同的是,通过Core Animation改变layer的状态(比如position),动画执行完毕后实际上是没有改变的(表面上看起来已改变)。

    优点

    1)性能强大,使用硬件加速,可以同时向多个图层添加不同的动画效果
    2)接口易用,只需要少量的代码就可以实现复杂的动画效果
    3)运行在后台线程中,在动画过程中可以响应交互事件(UIView动画默认动画过程中不响应交互事件)

    层次结构
    • CAAnimation:所有动画对象的父类,实现CAMediaTiming协议,负责控制动画的时间、速度和时间曲线等等,是一个抽象类,不能直接使用
    • CAPropertyAnimation:支持动画地显示图层的keyPath,一般不直接使用
    • CASpringAnimation:实现弹簧效果的动画
    • CAAnimationGroup:Group中所有动画并发执行,可以方便地实现需要多种类型动画的场景
    • CATransition:转场动画从一个场景以动画的形式过渡到另一个场景,如Push
    b、CABasicAnimation 和 CAKeyframeAnimation
    a、简介

    都是CAPropertyAnimation的子类,通过描绘路径来形成动画。CABasicAnimation通过设定起始点,终点,时间,可以看成是只有两个点的特殊的CAKeyFrameAnimation动画。CAKeyFrameAnimation则可以设置路径为更多的点构成的路径,动画会沿着我们设置的多个点进行移动

    常用属性
    • duration:动画的持续时间
    • repeatCount:动画持续次数,最大次数用MAXFLOAT
    • repeatDuration:设置动画的时间,在该时间内动画一直执行,不计次数
    • beginTime:指定动画开始的时间。从开始延迟几秒的话,设置为CACurrentMediaTime() +秒数的方式
    • timingFunction:设置动画的速度变化,timingFunctionskeyTimes的含义,设置每一小段路径上的动画的变化速率(CAKeyframeAnimation独有)
    kCAMediaTimingFunctionLinear:匀速运动,默认函数,立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹)
    kCAMediaTimingFunctionEaseIn:一个慢慢加速然后突然停止的方法。对于自由落体的例子来说很适合,或者对准一个目标的导弹的发射。
    kCAMediaTimingFunctionEaseOut:以一个全速开始,然后慢慢减速停止 有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地一声
    kCAMediaTimingFunctionEaseInEaseOut:慢慢加速然后再慢慢减速的过程 
    kCAMediaTimingFunctionDefault
    
    • fillMode:动画在开始和结束时的动作,默认值是kCAFillModeRemoved动画结束时是否执行逆动画
    kCAFillModeForwards :为了使动画结束之后layer保持结束状态,应将removedOnCompletion设置为NO
    kCAFillModeRemoved:动画将在设置的 beginTime 开始执行(如没有设置beginTime属性,则动画立即执行),动画执行完成后将会layer的改变恢复原状。
    
    • fromValu toValue byValue:所改变属性的起始值、结束值、改变量(CABasicAnimation独有)
    • values:关键帧数组对象,里面每一个元素即为一 个关键帧,动画会在对应的时间段内,依次执行数组中每一个关键帧的动画(CAKeyframeAnimation独有)
    • keyTimes:上面values设定了路径上的关键点,本参数则设定关键点之间的路径段上所需的时间,所以- keyTimes的个数应该比values的个数小1 (CAKeyframeAnimation独有 )
    • path:可以直接设置动画路径(CAKeyframeAnimation独有)
    c、CATransition
    • type:过渡动画的类型
    • subtype:设置转场方向
    • startProgress:开始进度,默认0.0.如果设置0.3,那么动画将从动画的0.3的部分开始
    • endProgress:结束进度,默认1.0.如果设置0.6,那么动画将从动画的0.6部分以后就会结束
    d、CASpringAnimation
    • mass:质量(影响弹簧的惯性,质量越大,弹簧惯性越大,运动的幅度越大)
    • stiffness:弹性系数(弹性系数越大,弹簧的运动越快)
    • damping:阻尼系数(阻尼系数越大,弹簧的停止越快)
    • initialVelocity:初始速率(弹簧动画的初始速度大小,弹簧运动的初始方向与初始速率的正负一致,若初始速率为0,表示忽略该属性)
    • settlingDuration:结算时间(根据动画参数估算弹簧开始运动到停止的时间,动画设置的时间最好根据此时间来设置)

    2、CABasicAnimation

    a、CABasicAnimation的渐变加载Demo
    加载
    LoadingView.h
    // view宽度必须使用此值
    FOUNDATION_EXPORT const CGFloat LoadingViewSize;
    
    // 全局loading
    @interface LoadingView : UIView
    
    /** 开始动画 */
    - (void)startAnimating;
    
    /** 结束动画 */
    - (BOOL)isAnimating;
    
    @end
    
    LoadingView.m
    const CGFloat LoadingViewSize = 200.0;// 加载视图的大小
    const CGFloat LoadingLayerWidth = 80.0;// 加载图层的宽度
    
    @interface LoadingView ()<CAAnimationDelegate>
    
    // 是否正在动画
    @property (nonatomic, assign, getter=isAnimating) BOOL animating;
    
    // 用于显示加载效果的渐变图层
    @property (nonatomic, strong) CAGradientLayer *loadingLayer;
    
    @end
    
    @implementation LoadingView
    
    @end
    
    绘图
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self)
        {
            self.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.5];
            self.layer.cornerRadius = 3.0;
            _animating = NO;// 未启动动画
     
            // 绘图
            [self drawLoadingLayer];
        }
        return self;
    }
    
    - (void)drawLoadingLayer
    {
        CGSize size = self.bounds.size;
        CGPoint center = CGPointMake(size.width/2, size.height/2);// 圆心
        CGFloat radius = LoadingLayerWidth/2;// 半径
        CGFloat strokeWidth = 2.0;// 描边宽度
        CGFloat angleDelta = M_PI / 2;// 角度变化量
    
    // 1.创建渐变图层
        // 初始化渐变图层
        _loadingLayer = [CAGradientLayer layer];
        _loadingLayer.frame = CGRectMake(center.x - LoadingLayerWidth/2, center.y - LoadingLayerWidth/2, LoadingLayerWidth, LoadingLayerWidth);
        [self.layer addSublayer:_loadingLayer];
        
        // 填充色不可用带透明度的颜色(会导致颜色叠加),要使用实色
        _loadingLayer.colors = @[(id)[UIColor whiteColor].CGColor, (id)[UIColor whiteColor].CGColor, (id)[UIColor redColor].CGColor];
        _loadingLayer.locations = @[@0.0, @0.5, @1.0];// 关键位置
        _loadingLayer.startPoint = CGPointMake(0.5, 0.0);// 开始点
        _loadingLayer.endPoint = CGPointMake(0.5, 1.0);// 结束点
        _loadingLayer.type = kCAGradientLayerAxial;// 线性渐变
        
    // 2.创建圆圈形状
        // 创建外部弧
        center = CGPointMake(radius, radius);
        UIBezierPath *maskPath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:angleDelta endAngle:angleDelta*4 clockwise:YES];
        
        // 添加右边弧
        CGFloat strokeRaduis = strokeWidth / 2;
        [maskPath addArcWithCenter:CGPointMake(center.x+radius-strokeRaduis, center.y) radius:strokeRaduis startAngle:0 endAngle:angleDelta*2 clockwise:YES];
        
        // 添加内部弧
        [maskPath addArcWithCenter:center radius:(radius-strokeWidth) startAngle:angleDelta*4 endAngle:angleDelta clockwise:NO];
        
        // 添加底边弧
        [maskPath addArcWithCenter:CGPointMake(center.x, center.y+radius-strokeRaduis) radius:strokeRaduis startAngle:-angleDelta endAngle:angleDelta clockwise:YES];
        
        // 完成路径绘制,闭合路径
        [maskPath closePath];// 注释掉好像也没影响
        
        // 添加路径到形状图层上,_loadingLayer没有path属性,只有mask属性
        CAShapeLayer *maskLayer = [CAShapeLayer layer];
        maskLayer.frame = _loadingLayer.bounds;// 二者重合
        maskLayer.path = maskPath.CGPath;
        
        // 3.将形状图层作为渐变图层的遮罩,不加这句得到的只是个旋转的矩形
        _loadingLayer.mask = maskLayer;
    }
    
    配置动画
    // 展示视图动画
    - (void)showAnimation
    {
        // 透明度动画
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
        animation.fromValue = @(0.0);// 初始值
        animation.toValue = @(1.0);// 结束值
        animation.removedOnCompletion = NO;// 完成后不移除
        animation.duration = 0.3;// 持续0.3秒
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];// 渐渐退出
        // 添加到视图图层中
        [self.layer addAnimation:animation forKey:@"showAnimation"];
    }
    
    // 隐藏视图动画
    - (void)hideAnimation
    {
        // 透明度动画
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
        animation.fromValue = @(1.0);// 初始值
        animation.toValue = @(0.0);// 结束值
        animation.removedOnCompletion = NO;// 完成后不移除
        animation.duration = 0.3;// 持续0.3秒
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];// 渐渐退出
        // 添加到视图图层中
        [self.layer addAnimation:animation forKey:@"hideAnimation"];
    }
    
    // 加载视图动画
    - (void)loadingAnimating
    {
        // 绕Z轴旋转动画
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
        animation.fromValue = @(0);// 初始值
        animation.toValue = @(M_PI*2);// 结束值
        animation.removedOnCompletion = NO;// 完成后不移除
        animation.repeatCount = HUGE_VALF;// 重复无限次
        animation.duration = 1.0;// 持续1秒
        // 添加到加载图层中
        [_loadingLayer addAnimation:animation forKey:@"loadingAnimating"];
    }
    
    操作
    // 开始动画
    - (void)startAnimating
    {
        // 已经在动画状态中则直接返回
        if (_animating)
        {
            return;
        }
        
        // 清除之前的旧动画
        [self cleanAnimations];
        
        // 设置为动画状态
        _animating = YES;
        // 不再隐藏视图
        self.hidden = NO;
        
        // 展示视图动画
        [self showAnimation];
        // 加载视图动画
        [self loadingAnimating];
    }
    
    // 停止动画
    - (void)stopAnimating
    {
        // 清除动画
        [self cleanAnimations];
        // 隐藏视图
        self.hidden = YES;
        // 设置为非动画状态
        _animating = NO;
    }
    
    // 清除动画
    - (void)cleanAnimations
    {
        [self.layer removeAnimationForKey:@"showAnimating"];// 移除显示动画
        [self.layer removeAnimationForKey:@"hideAnimating"];// 移除隐藏动画
        [_loadingLayer removeAnimationForKey:@"loadingAnimating"];// 移除加载动画
    }
    
    使用
    // 渐变
    - (void)createLoadingView
    {
        // 创建视图
        self.loadingView = [[LoadingView alloc] initWithFrame:CGRectMake(100, 300, LoadingViewSize, LoadingViewSize)];
        [self.view addSubview:self.loadingView];
        
        // 开始动画
        [self.loadingView startAnimating];
    }
    

    3、CAKeyframeAnimation

    a、8个球加载Demo
    8个球
    BallsLoadingView.h文件
    FOUNDATION_EXPORT const CGFloat BallsLoadingLayerSize;
    
    @interface BallsLoadingView : UIView
    
    /** 开始动画 */
    - (void)startAnimation;
    
    /** 结束动画 */
    - (void)stopAnimation;
    
    @end
    
    BallsLoadingView.m文件
    const CGFloat BallsLoadingLayerSize = 100.0;// 加载图层的大小
    
    @interface BallsLoadingView ()
    
    @property (nonatomic, strong) CALayer *loadingLayer;// 加载图层
    
    @end
    
    @implementation BallsLoadingView
    
    @end
    
    绘图
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self)
        {
            [self drawInnerLoadingLayer];
        }
        return self;
    }
    
    // 加载视图为8个球
    - (void)drawInnerLoadingLayer
    {
        CGSize size = self.bounds.size;
        CGFloat radius = size.width/2;// 半径
        CGFloat dotRadius = 7.5 * (size.width/BallsLoadingLayerSize);// 球的半径
        CGFloat postionRadius = radius - dotRadius;// 距离
        CGFloat angleDelta = M_PI / 4;// 角度变化量
        
        // 1.创建加载图层
        _loadingLayer = [CALayer layer];
        _loadingLayer.frame = self.bounds;
        [self.layer addSublayer:_loadingLayer];
        
        
        // 2.绘制8个球
        UIColor *ballColor = [UIColor blueColor];// 球的颜色
        for (int i=7; i>=0; I--)
        {
            // 计算每个球的圆心
            CGFloat postionX = radius + postionRadius * cos(angleDelta*i);
            CGFloat postionY = radius + postionRadius * sin(angleDelta*i);
            
            // 绘制每个球的路径
            UIBezierPath *dotPath = [UIBezierPath bezierPath];
            [dotPath addArcWithCenter:CGPointZero radius:dotRadius startAngle:0 endAngle:M_PI*2 clockwise:YES];
            [dotPath closePath];
            
            // 创建形状图层
            CAShapeLayer *dotLayer = [CAShapeLayer layer];
            dotLayer.position = CGPointMake(postionX, postionY);// 球的放置位置
            dotLayer.anchorPoint = CGPointZero;// 球的锚点
            dotLayer.path = dotPath.CGPath;// 绘制路径
            dotLayer.fillColor = ballColor.CGColor;// 填充颜色
            
            // 将形状图层添加到加载图层上
            [_loadingLayer addSublayer:dotLayer];
        }
    }
    
    配置动画
    // 小球依次缩放和透明
    - (void)configureAnimation
    {
        // 获取加载图层中的每个球图层
        NSArray<CALayer *> *sublayers = _loadingLayer.sublayers;
        // 球的数量
        NSUInteger count = sublayers.count;
        // 依次延迟0.12秒展示
        CGFloat delayDelta = 0.12;
        
        // 依次为每个球添加动画
        for (int i=0; i<count; I++)
        {
            // 获取每个球的图层
            CALayer *subLayer = sublayers[I];
            
            NSArray *scales = @[@1.0, @0.4, @1.0];// 缩放 1->0.4->1
            NSArray *opacities = @[@1.0, @0.3, @1.0];// 透明度 1->0.3->1
            NSArray *keyTimes = @[@0, @0.5, @1.0];// 关键时刻 0->0.5->1
            CGFloat duration = 2.0;// 时长
            
            // xScale: X轴上的缩放动画
            CAKeyframeAnimation *xScaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.x"];
            xScaleAnimation.values = scales;
            xScaleAnimation.keyTimes = keyTimes;
            xScaleAnimation.duration = duration;
            
            // yScale: Y轴上的缩放动画
            CAKeyframeAnimation *yScaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.y"];
            yScaleAnimation.values = scales;
            yScaleAnimation.keyTimes = keyTimes;
            yScaleAnimation.duration = duration;
            
            // opacity: 透明度动画
            CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
            opacityAnimation.values = opacities;
            opacityAnimation.keyTimes = keyTimes;
            opacityAnimation.duration = duration;
            
            // 组合动画
            CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
            groupAnimation.animations = @[xScaleAnimation, yScaleAnimation, opacityAnimation];
            groupAnimation.duration = duration;
            groupAnimation.removedOnCompletion = NO;// 完成后不移除动画
            groupAnimation.repeatCount = HUGE_VALF;// 无限次重复
            groupAnimation.beginTime = delayDelta * i;// 依次延迟0.12秒展示
            
            // 将组合动画添加到球图层上
            [subLayer addAnimation:groupAnimation forKey:@"groupAnimation"];
        }
    }
    
    操作
    // 开始动画
    - (void)startAnimation
    {
        [self configureAnimation];
    }
    
    // 结束动画
    - (void)stopAnimation
    {
        // 获取加载图层中的每个球图层
        NSArray<CALayer *> *sublayers = _loadingLayer.sublayers;
        NSUInteger count = sublayers.count;
        
        for (int i=0; i<count; I++)
        {
            // 获取每个球的图层
            CALayer *subLayer = sublayers[I];
            // 移除球图层上的所有动画
            [subLayer removeAllAnimations];
        }
    }
    
    使用
    // 8个球
    - (void)createBallsLoadingView
    {
        // 创建视图
        self.ballsLoadingView = [[BallsLoadingView alloc] initWithFrame:CGRectMake(150, 300, BallsLoadingLayerSize, BallsLoadingLayerSize)];
        [self.view addSubview:self.ballsLoadingView];
        
        // 开始动画
        [self.ballsLoadingView startAnimation];
    }
    

    4、CATransition

    a、过渡到蓝色视图
    过渡到蓝色视图
    TransitionViewController
    // 过渡动画视图
    @interface TransitionViewController : UIViewController
    
    @property (nonatomic, strong) UIView *blueView;
    
    @end
    
    @implementation TransitionViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        self.blueView = [[UIView alloc] initWithFrame:CGRectZero];
        self.blueView.backgroundColor = [UIColor blueColor];
        [self.view addSubview:self.blueView];
        [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
            make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom);
            make.left.equalTo(self.view.mas_left);
            make.right.equalTo(self.view.mas_right);
        }];
    }
    
    @end
    
    CoreAnimationViewController
    // 过渡动画
    - (void)transitionAnimation
    {
        TransitionViewController *transitonVC = [[TransitionViewController alloc] init];
        [self.navigationController pushViewController:transitonVC animated:NO];
        
        CATransition *transition = [CATransition animation];
        transition.type = kCATransitionPush;// Push方式
        transition.subtype = kCATransitionFromLeft;// 从左部滑出
        // 在图层上添加动画
        [transitonVC.view.layer addAnimation:transition forKey:@"transition"];
    }
    

    5、CASpringAnimation

    6、CAAnimationGroup

    a、波纹动画
    波纹动画
    RippleLayer.h文件
    #import <QuartzCore/QuartzCore.h>
    
    @interface RippleLayer : CALayer
    
    /** 开始动画 */
    - (void)startAnimation;
    
    /** 结束动画 */
    - (void)stopAnimation;
    
    @end
    
    RippleLayer.m文件
    #import <FLAnimatedImage/FLAnimatedImage.h>
    
    @interface RippleLayer ()<CAAnimationDelegate>// 动画委托
    
    // 计时器
    @property (nonatomic, strong) NSTimer *timer;
    
    @end
    
    @implementation RippleLayer
    
    @end
    
    创建波纹图
    - (CAShapeLayer *)rippleLayer
    {
        // 路径
        CGSize size = self.bounds.size;
        CGFloat raduis = size.width/2;// 半径
        CGPoint center = CGPointMake(size.width/2, size.height/2);// 圆心
        UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:raduis startAngle:0 endAngle:M_PI*2 clockwise:YES];// 圆
        [path closePath];// 闭合
        
        UIColor *strokeColor = [UIColor colorWithRed:189.0/255 green:141.0/255 blue:4.0/255 alpha:0.45];// 描边颜色
        UIColor *fillColor = [UIColor colorWithRed:189.0/255 green:141.0/255 blue:4.0/255 alpha:0.08];// 填充颜色
        
        // 图层
        CAShapeLayer *layer = [CAShapeLayer layer];
        layer.path = path.CGPath;
        layer.lineWidth = 0.5;
        layer.strokeColor = strokeColor.CGColor;
        layer.fillColor = fillColor.CGColor;
        
        // 勿改变设置顺序
        layer.anchorPoint = CGPointMake(0.5, 0.5);// 铆点 0~1 这里指视图中心
        layer.frame = self.bounds;
        layer.transform = CATransform3DMakeScale(1.5, 1.5, 1.0);// 缩放
        layer.opacity = 0.0;// 透明
        
        return layer;
    }
    
    配置波纹动画:透明度 + 缩放
    - (void)fireRippleAnimation
    {
        // 添加轮胎图层
        CAShapeLayer *layer = [self rippleLayer];
        [self addSublayer:layer];
        
    // scalePath
        UIBezierPath *scalePath = [UIBezierPath bezierPath];
        [scalePath moveToPoint:CGPointMake(1.5, 0)];
        [scalePath addQuadCurveToPoint:CGPointMake(1.8, 1.0) controlPoint:CGPointMake(1.2, 0.2)];
        [scalePath addQuadCurveToPoint:CGPointMake(3.0, 3.0) controlPoint:CGPointMake(3.0, 2.5)];
        
    // opacityPath
        UIBezierPath *opacityPath = [UIBezierPath bezierPath];
        [opacityPath moveToPoint:CGPointZero];
        [opacityPath addLineToPoint:CGPointMake(0.8, 0.2)];
        [opacityPath addLineToPoint:CGPointMake(0.0, 2.5)];
        [opacityPath addLineToPoint:CGPointMake(0.0, 3.0)];
        
        CFTimeInterval duration = 3.0;
        
    // ScaleAnimation
        CAKeyframeAnimation *xScaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.x"];
        xScaleAnimation.calculationMode = kCAAnimationPaced;
        xScaleAnimation.path = scalePath.CGPath;
        xScaleAnimation.duration = duration;
        
        CAKeyframeAnimation *yScaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.y"];
        yScaleAnimation.calculationMode = kCAAnimationPaced;
        yScaleAnimation.path = scalePath.CGPath;
        yScaleAnimation.duration = duration;
        
    // OpacityAnimation
        CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
        opacityAnimation.calculationMode = kCAAnimationPaced;
        opacityAnimation.path = opacityPath.CGPath;
        opacityAnimation.duration = duration;
        
    // GroupAnimation
        CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
        groupAnimation.animations = @[xScaleAnimation,yScaleAnimation,opacityAnimation];
        groupAnimation.duration = duration;
        groupAnimation.delegate = self;
        groupAnimation.removedOnCompletion = NO;
        
        // 给图层添加动画
        [layer addAnimation:groupAnimation forKey:@"groupAnimation"];
    }
    
    操作
    // 开始动画
    - (void)startAnimation
    {
        // 已经在计时则直接返回
        if (_timer)
        {
            return;
        }
        
        // 代理机制防止计时器的循环引用
        FLWeakProxy *target = [FLWeakProxy weakProxyForObject:self];
        _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:target selector:@selector(fireRippleAnimation) userInfo:nil repeats:YES];
    }
    
    // 结束动画
    - (void)stopAnimation
    {
        [_timer invalidate];
        _timer = nil;
    }
    
    // 动画停止时调用
    - (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag
    {
        // 删除最低部的layer
        CALayer *layer = [self.sublayers firstObject];
        [layer removeFromSuperlayer];
    }
    
    使用
    - (void)createRippleLayerView
    {
        // 创建View
        CGSize viewSize = self.view.bounds.size;
        UIView *rippleView = [[UIView alloc] initWithFrame:CGRectMake(viewSize.width/2, 300, 100, 100)];
        
        // 创建Layer
        self.rippleLayer = [RippleLayer layer];
        self.rippleLayer.frame = rippleView.bounds;
        
        // 将Layer添加到View
        [rippleView.layer addSublayer:self.rippleLayer];
        // 添加View
        [self.view addSubview:rippleView];
        
        // 启动动画
        [self.rippleLayer startAnimation];
    }
    

    7、CADisplayLink

    a、下拉加载动画
    下拉加载动画
    RefreshView.h
    #import <UIKit/UIKit.h>
    #import <QuartzCore/QuartzCore.h>
    #import <CoreGraphics/CoreGraphics.h>
    
    FOUNDATION_EXPORT const CGFloat PullLoadingViewSize;
    
    @interface RefreshView : UIView
    
    /** 关联滑动视图 */
    @property (weak, nonatomic) UIScrollView *scrollView;
    
    /** 拉多少距离转一周 */
    @property (nonatomic, assign) CGFloat distanceForTurnOneCycle;
    
    /** 下拉加载 */
    - (void)loading;
    
    /** 当取消或者加载完成的时机会调用这个方法 */
    - (void)loadingFinished;
    
    @end
    
    RefreshView.m
    const CGFloat PullLoadingViewSize = 30.0;
    
    @interface RefreshView ()
    
    @property (nonatomic, strong) CALayer *wheelLayer;
    @property (nonatomic, strong) NSArray *windLayers;
    @property (nonatomic, strong) NSArray *windOffsets;
    
    @property (nonatomic, strong) CADisplayLink *displayLink;
    
    @end
    
    @implementation RefreshView
    
    @end
    
    绘制视图
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self)
        {
            [self drawRefreshView];
        }
        return self;
    }
    
    // 绘制加载风车视图
    - (void)drawRefreshView
    {
        // 下拉80点转动一周
        _distanceForTurnOneCycle = 80;
        // 类似计时器,但每一帧都会调用
        _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayDidRefresh)];
        // 放到NSRunLoopCommonModes,防止下拉切换mode导致计时器失效
        [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        
    
        CGFloat halfWidth = self.bounds.size.width / 2;// 视图一半
        CGPoint center = CGPointMake(halfWidth, halfWidth);// 轮胎中心点
        
        // 创建轮胎视图
        _wheelLayer = [CALayer layer];
        _wheelLayer.frame = self.bounds;
        [self.layer addSublayer:_wheelLayer];
        
    //拆分为两部分,轮胎使用描边,扇叶使用填充
        //轮胎描边
        UIBezierPath *wheelMaskPath = [UIBezierPath bezierPathWithArcCenter:center radius:halfWidth startAngle:0 endAngle:M_PI*2 clockwise:YES];// 路径为外圆
        CAShapeLayer *wheelMask = [CAShapeLayer layer];// 外层轮胎图层
        wheelMask.frame = self.bounds;
        wheelMask.path = wheelMaskPath.CGPath;// 外层轮胎图层上的路径
        
        CGFloat tyreRadius = 23/2 - 1.5;// 内层轮胎半径
        UIColor *tyreColor = [UIColor redColor];// 内层轮胎颜色为红色
        CAShapeLayer *tyreLayer = [CAShapeLayer layer];// 内层轮胎图层
        UIBezierPath *tyrePath =[UIBezierPath bezierPathWithArcCenter:center radius:tyreRadius startAngle:0 endAngle:M_PI * 2 clockwise:YES];// 路径为内圆
        [tyrePath closePath];// 关闭路径
        tyreLayer.path = tyrePath.CGPath;// 内层轮胎图层上的路径
        tyreLayer.strokeColor = tyreColor.CGColor;// 描边颜色
        tyreLayer.lineWidth = 3;// 描边宽度
        tyreLayer.fillColor = [UIColor clearColor].CGColor;// 填充颜色
        tyreLayer.frame = self.bounds;
        tyreLayer.mask = wheelMask;// 遮罩
        [_wheelLayer addSublayer:tyreLayer];// 将形状图层添加到轮胎图层上
        
        // 扇叶
        CGFloat innerRadius = 7/2;
        CGFloat outerRadius = 15/2;
        CGFloat angleDelta = M_PI * 2 / 5;// 转动角度
        CGFloat arcAngle = angleDelta * 2 / 3;// 圆弧角度
        
        UIBezierPath *maskPath = [UIBezierPath bezierPath];// 遮罩路径
        for (int i=0; i<5; I++)
        {
            CGFloat startAngle = angleDelta * i;// 开始角度
            CGFloat endAngle = startAngle + arcAngle;// 开始角度 + 弧度 = 结束角度
            CGPoint innerArcStart = [self calcPointWithAngle:endAngle radius:innerRadius center:center];// 计算开始点
            CGPoint outerArcStart = [self calcPointWithAngle:startAngle radius:outerRadius center:center];// 计算结束点
            
            // 绘制路径
            UIBezierPath *path = [UIBezierPath bezierPath];
            // 外部弧
            [path addArcWithCenter:center radius:outerRadius startAngle:startAngle endAngle:endAngle clockwise:YES];
            // 连线
            [path addLineToPoint:innerArcStart];
            // 内部弧
            [path addArcWithCenter:center radius:innerRadius startAngle:endAngle endAngle:startAngle clockwise:NO];
            // 连线
            [path addLineToPoint:outerArcStart];
            // 闭合
            [path closePath];
            [maskPath appendPath:path];
        }
        
        // 内部环
        CGFloat dotRadius = 3.0 / 2;// 点半径
        UIBezierPath *dotPath = [UIBezierPath bezierPathWithArcCenter:center radius:dotRadius startAngle:0 endAngle:M_PI*2 clockwise:YES];// 画圆
        [dotPath closePath];// 闭合
        [maskPath appendPath:dotPath];// 添加到遮罩路径
        
        // 遮罩图层
        CAShapeLayer *maskLayer = [CAShapeLayer layer];
        maskLayer.frame = self.bounds;
        maskLayer.path = maskPath.CGPath;
        
        UIColor *fanBladeColor = [UIColor redColor];// 风扇颜色
        CALayer *fanBladeLayer = [CALayer layer];// 风扇图层
        fanBladeLayer.backgroundColor = fanBladeColor.CGColor;// 背景颜色为红色
        fanBladeLayer.frame = self.bounds;
        fanBladeLayer.mask = maskLayer;// 遮罩图层
        // 也可将fanBladeLayer添加到tyreLayer中
        [tyreLayer addSublayer:fanBladeLayer];
        
    // 风
        UIColor *windColor = [UIColor redColor];// 风的颜色为红色
        NSMutableArray *windLayers = [NSMutableArray array];
        for (int i=0; i<3; I++)
        {
            // 创建风图层
            CAShapeLayer *layer = [CAShapeLayer layer];// 图层
            UIBezierPath *path = [UIBezierPath bezierPath];
            [path moveToPoint:CGPointMake(0, 0)];
            [path addLineToPoint:CGPointMake(6, 0)];// 绘制线条
            layer.path = path.CGPath;// 图层的线条
            layer.strokeColor = windColor.CGColor;// 描边颜色为风的颜色
            layer.lineWidth = 1.0;// 描边宽度
            layer.position = [self resetWindLayerPositionWithIndex:i];// 根据index计算风视图的开始位置
            layer.opacity = 0.0;// 刚开始透明
            [self.layer addSublayer:layer];// 添加风图层
            [windLayers addObject:layer];// 放到风图层的集合中
        }
        _windLayers = windLayers;
        _windOffsets = @[@18, @23, @16];// 根据index计算风视图的X轴上的坐标偏移量
    }
    
    让视图动起来
    // 轮胎动画
    - (void)wheelAnimation
    {
        // 设置Z轴上的旋转动画
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
        animation.fromValue = @(M_PI*2);
        animation.toValue = @(0);// 转动一圈
        animation.removedOnCompletion = NO;// 完成后不移除
        animation.repeatCount = HUGE_VALF;// 重复
        animation.duration = 1.0;// 1秒
        [_wheelLayer addAnimation:animation forKey:@"loading"];// 作为加载动画
    }
    
    // 吹风动画
    - (void)windAnimation
    {
        // 为3个风图层分别添加动画
        for (int i=0; i<3; I++)
        {
            [self addAnimationToWindLayerForIndex:i];
        }
    }
    
    // 在风视图上添加动画
    - (void)addAnimationToWindLayerForIndex:(NSInteger)index
    {
        CGFloat duration = 1.5;// 1.5秒
    
    // 移动动画
        // 根据index计算风视图的开始位置
        CGPoint startPosition = [self resetWindLayerPositionWithIndex:index];
        // 根据index计算风视图的X轴上的坐标偏移量
        CGFloat offsetX = [_windOffsets[index] floatValue];
        // 风视图的结束位置
        CGPoint endPosition = CGPointMake(startPosition.x+offsetX, startPosition.y);
        
        // 设置关键位置:开始->结束->开始
        NSArray *positions = @[[NSValue valueWithCGPoint:startPosition],
                               [NSValue valueWithCGPoint:endPosition],
                               [NSValue valueWithCGPoint:endPosition],
                               [NSValue valueWithCGPoint:startPosition]];
        // 设置时间点
        NSArray *positionKeyTimes = @[@0, @(1.0/duration), @(1.3/duration), @1.0];
        // 设置动画效果
        NSArray *positionFunctions = @[[CAMediaTimingFunction functionWithName:
                                       kCAMediaTimingFunctionEaseOut],
                                      [CAMediaTimingFunction functionWithName:
                                       kCAMediaTimingFunctionLinear],
                                      [CAMediaTimingFunction functionWithName:
                                       kCAMediaTimingFunctionLinear]];
        
    // 透明度动画
        // 设置关键点的透明度
        NSArray *opacities = @[@0.0, @1.0, @1.0, @0.0, @0.0];
        // 设置时间点
        NSArray *opacityKeyTimes = @[@0, @(0.3/duration), @(1.0/duration), @(1.3/duration), @1.0];
        // 设置动画效果
        NSArray *opacityFunctions = @[[CAMediaTimingFunction functionWithName:
                                    kCAMediaTimingFunctionEaseOut],
                                   [CAMediaTimingFunction functionWithName:
                                    kCAMediaTimingFunctionLinear],
                                   [CAMediaTimingFunction functionWithName:
                                    kCAMediaTimingFunctionEaseOut],
                                   [CAMediaTimingFunction functionWithName:
                                    kCAMediaTimingFunctionLinear]];
        
        // 根据index获取创建好的风图层,并将其本地化
        CALayer *windLayer = _windLayers[index];
        
        // 创建透明度动画
        CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
        opacityAnimation.values = opacities;
        opacityAnimation.keyTimes = opacityKeyTimes;
        opacityAnimation.timingFunctions = opacityFunctions;
        
        // 创建位置动画
        CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        positionAnimation.values = positions;
        positionAnimation.keyTimes = positionKeyTimes;
        positionAnimation.timingFunctions = positionFunctions;
        
        // 创建组合动画
        CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
        groupAnimation.animations = @[opacityAnimation, positionAnimation];
        groupAnimation.duration = duration;
        groupAnimation.repeatCount = HUGE_VALF;// 重复无限次
        
        // 将组合动画作为风动画添加到风图层上
        [windLayer addAnimation:groupAnimation forKey:@"wind"];
    }
    
    // 根据index计算风视图的开始位置
    - (CGPoint)resetWindLayerPositionWithIndex:(NSInteger)index
    {
        CGFloat positionX = self.bounds.size.width/2 + 23/2 - 3;
        CGFloat positionYDelta = 5;
        CGFloat positionYStart = self.bounds.size.width/2 - positionYDelta;
        return CGPointMake(positionX, positionYStart + positionYDelta*index);
    }
    
    - (CGPoint)calcPointWithAngle:(CGFloat)angle radius:(CGFloat)radius center:(CGPoint)center
    {
        // 正弦和余弦函数计算做坐标点
        CGFloat postionX = center.x + radius * cos(angle);
        CGFloat postionY = center.y + radius * sin(angle);
        return CGPointMake(postionX, postionY);
    }
    
    和scrollView联动
    // 滚动视图存在则实现下拉加载联动效果
    - (void)displayDidRefresh
    {
        if (_scrollView)
        {
            // 总下拉距离
            CGFloat distance = _scrollView.contentOffset.y;
            // 2PI *(总下拉距离 / 80)= 转动几圈
            CGFloat angle = - (M_PI * 2 * distance / _distanceForTurnOneCycle);
            // 仿射变换实现旋转效果
            CGAffineTransform transform = CGAffineTransformMakeRotation(angle);
            // 在轮胎图层上添加旋转动画
            [_wheelLayer setAffineTransform:transform];
        }
    }
    
    操作方法
    // 下拉加载,开始风和轮胎动画
    - (void)loading
    {
        // 停止关联
        _displayLink.paused = YES;
        [self wheelAnimation];
        [self windAnimation];
    }
    
    // 当取消或者加载完成的时机会调用这个方法
    - (void)loadingFinished
    {
        // 启动关联
        _displayLink.paused = NO;
        // 移除加载动画
        [_wheelLayer removeAnimationForKey:@"loading"];
        for (CALayer *layer in _windLayers)
        {
            [layer removeAllAnimations];
        }
    }
    
    - (void)dealloc
    {
        [_displayLink invalidate];
    }
    
    使用
    @interface RefreshViewController() <UIScrollViewDelegate>
    
    @property(nonatomic, strong) RefreshView *refreshView;
    
    @end
    
    @implementation RefreshViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(10, 500, 200, 300)];
        scrollView.contentSize = CGSizeMake(200, 600);
        scrollView.backgroundColor = [UIColor yellowColor];
        scrollView.delegate = self;
        [self.view addSubview:scrollView];
        
        RefreshView *refreshView = [[RefreshView alloc] initWithFrame:CGRectMake(100, 100, PullLoadingViewSize, PullLoadingViewSize)];
        refreshView.scrollView = scrollView;
        [self.view addSubview:refreshView];
        self.refreshView = refreshView;
    }
    
    // 下拉开始的时候加载
    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
    {
        [self.refreshView loading];
    }
    
    // 下拉减速的时候结束加载
    - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
    {
        [self.refreshView loadingFinished];
    }
     
    @end
    

    Demo

    Demo在我的Github上,欢迎下载。
    AnimationDemo

    参考文献

    相关文章

      网友评论

          本文标题:IOS基础:动画

          本文链接:https://www.haomeiwen.com/subject/dmwrmktx.html