美文网首页iOS 开发每天分享优质文章基础应用
CAAnimation:属性动画CABasicAnimation

CAAnimation:属性动画CABasicAnimation

作者: pro648 | 来源:发表于2021-03-04 22:55 被阅读0次
    CoreAnimationXmind.png

    这是 Core Animation 的系列文章,介绍了 Core Animation 的用法,以及如何进行性能优化。

    1. CoreAnimation基本介绍
    2. CGAffineTransform和CATransform3D
    3. CALayer及其各种子类
    4. CAAnimation:属性动画CABasicAnimation、CAKeyframeAnimation以及过渡动画、动画组
    5. 图层时间CAMediaTiming
    6. 计时器CADisplayLink
    7. 影响动画性能的因素及如何使用 Instruments 检测
    8. 图像IO之图片加载、解码,缓存
    9. 图层性能之离屏渲染、栅格化、回收池

    上一篇文章介绍了 Core Animation 的各种图层类,这一篇文章将介绍显式动画(explicit animation),显式动画允许为指定属性添加动画,或创建非线性动画(如沿指定曲线运动)。

    1. CAAnimation

    CAAnimation是 Core Animation 中所有动画的超类,是抽象类。

    CAAnimation提供了对CAMediaTimingCAAction协议的支持。不要创建CAAnimation实例管理 Core Animation 图层动画或 SceneKit 对象,一般使用CABasicAnimationCAKeyframeAnimationCASpringAnimationCAAnimationGroupCATransition动画。

    isRemovedOnCompletion属性决定动画完成后是否自动将动画图层从层级结构中移除,默认为true。如果设置为false,需手动移除,否则会有内存泄漏。

    timingFunction定义了动画的时间函数,默认为nil,即线性动画。

    delegate属性指定委托对象,默认为nilCAAnimationDelegate协议提供了动画开始、结束事件。

    2. CAPropertyAnimation

    继承自CAAnimation的抽象类,用于创建可操纵图层属性值的动画。

    不要使用CAPropertyAnimation实例管理 Core Animaiton 的属性动画,应使用CAPropertyAnimation的子类CABasicAnimationCAKeyframeAnimation,或CABasicAnimation的子类CASpringAnimation

    使用keyPath指定要设置动画的属性,keyPath是用点表示法指向层级关系中任意属性,而非仅仅是属性名称。例如,不仅可以对position添加动画,还可以对position.x添加动画。

    3. CABasicAnimation

    CABasicAnimation继承自CAPropertyAnimation,为图层单个属性提供动画。

    使用继承的init(keyPath:)方法指定属性,以在 render tree 中执行动画。

    3.1 常用属性

    CABasicAnimation增加了fromValuetoValuebyValue,这三个属性定义了动画要插入的值,所有这些属性都是可选的,但不能同时使用三个。属性值类型应与keyPath类型匹配。因为属性动画可能是颜色渐变、位置移动、变换等,所以,fromValuetoValuebyValue类型都是Any。

    有以下几种组合使用方式:

    • fromValuetoValue非空,动画在fromValuetoValue之间插入。
    • fromValuebyValue非空,动画在fromValuefromValue + byValue之间插入。
    • byValuetoValue非空,动画在toValue - byValuetoValue之间插入。
    • fromValue非空,动画在fromValue和当前 presentation 值之间插入。
    • toValue非空,动画在 presentation 当前值和toValue值之间插入。
    • byValue非空,动画在 presentation 当前值和 presentation + byValue 之间插入。
    • 所有值都为空,动画在 presentation layer 之前值和 presentation layer 当前值之间插入。

    3.2 设置动画属性

    CABasicAnimation可用动画形式改变标量属性,如opacity

            let animation = CABasicAnimation(keyPath: "opacity")
            animation.fromValue = 0
            animation.toValue = 1
            layerView.layer.add(animation, forKey: nil)
    

    非标量属性也可以设置动画,如backgroundColor。Core Animation 会自动在fromValuetoValue之间插入中间值。下面代码使用动画将背景色从当前颜色更改为红色:

            let animation = CABasicAnimation(keyPath: "backgroundColor")
            animation.toValue = UIColor.red.cgColor
            layerView.layer.add(animation, forKey: nil)
    

    具有多个值的非标量属性(如boundsposition),为fromValuebyValuetoValue传入数组:

            let animation = CABasicAnimation(keyPath: "position")
            animation.fromValue = [0, 100]
            animation.toValue = [100, view.bounds.size.height]
            layerView.layer.add(animation, forKey: nil)
    

    keyPath可以访问属性单独组件。下面代码拉伸了图层transformy

            let animation = CABasicAnimation(keyPath: "transform.scale.y")
            animation.duration = 2
            animation.byValue = 0.5
            animation.toValue = 3
            layerView.layer.add(animation, forKey: nil)
    

    可以看到动画插入值是从 y 值为 3-0.5(即2.5)开始,3结束。

    3.3 更新 layer model

    隐式动画是修改图层属性时自动产生的动画,修改视图的属性不会产生隐式动画。显式动画只是动画,不会修改图层 model,动画结束后默认自动从图层移除。因此,需要更新 layer model。

    如上面的缩放 y 可以使用下面任一方法更新 layer model:

            // 1. 使用CATransform3D更新 layer model
            var transform = CATransform3DIdentity
            transform = CATransform3DScale(transform, 1, 3, 1)
            layerView.layer.transform = transform
            
            // 2. 使用 CGAffineTransform 更新 layer model
            layerView.transform = CGAffineTransform(scaleX: 1, y: 3)
    

    虽然,通过设置isRemovedOnCompletionfalse也可以达到同样效果,但应避免这样做。一方面,将动画保留到屏幕中会影响性能;另一方面,presentation layer 与 model layer 一致可以降低复杂度,有利于后期维护。

    你可以自行更新其他动画 model,如果有问题可以在文章底部下载源码查看。

    4. CASpringAnimation

    CASpringAnimation可以将弹簧类似弹性效果添加到图层属性。CASpringAnimation继承自CABasicAnimaiton

    可以把CASpringAnimation设想为摆钟,在理想状态下(即没有阻力),会持续同样振幅的摆动:

    CASpringTick.png

    震动曲线如下:

    CASpringFrictionless.png

    在真实世界中,由于阻力的作用,其摆动效果如下:

    CASpringLoseEnergy.png

    震动曲线如下:

    CASpringFriction.png

    4.1 常用属性

    CASpringAnimation的以下属性可以控制弹性效果:

    • damping:减震。damping属性定义抑制弹簧运动的摩擦力大小。默认值为10。减小damping值摩擦力变小,弹簧晃动次数增加,settlingDuration可能大于duration。增大damping值阻力变大,弹簧晃动次数减少,settlingDurationduration小。
    • initialVelocity:初始速度,默认为0,表示静止的对象。负值表示与目标位置相反方向的初始速度,正值表示与目标位置相同方向的初始速度。
    • mass:附着在弹簧末端物体的重量,默认值为1。增加该值会增大弹性效果,即震动次数更多、幅度更大,settlingDuration也会增加。减小mass会减弱弹性效果。
    • settlingDuration:弹性动画完全静止所需预期时间,可能和duration不一致。
    • stiffness:弹簧钢性系数,默认值为100。增大stiffness会减少震动次数,减小settlingDuration时间。减少stiffness会增加震动次数,增加settlingDuration时间。

    4.2 设置弹簧动画

    下面使用CASpringAnimation创建一个弹簧动画,点击按钮时上下摆动文本框,并使用红色描边。如下所示:

        private func testSpringAnimation() {
            let jump = CASpringAnimation(keyPath: "position.y")
            jump.fromValue = textField.layer.position.y + 1.0
            jump.toValue = textField.layer.position.y
            jump.duration = 0.25
            textField.layer.add(jump, forKey: nil)
        }
    

    目前,动画只会将文本框向下移动1point,然后回到初始位置。

    CASpringAnimation添加以下属性:

                    jump.initialVelocity = 100.0
            jump.mass = 10.0
            jump.stiffness = 1500.0
            jump.damping = 50.0
    

    修改弹簧变量可能并不容易,你可以反复修改这些值,观察其中区别以实现最佳效果。

    多次运行demo,会发现动画运行到一定位置后直接跳到了终点。这是由于duration设置为了0.25秒,但弹簧动画在0.25秒内并不能完成。下图显示了弹簧动画如何被切断:

    CASpringCutOff.png

    想要修复这一问题,只需设置durationsettlingDuration即可。

            jump.duration = jump.settlingDuration
    

    此外,还可以弹性设置描边颜色。如下所示:

            textField.layer.borderWidth = 2.0
            textField.layer.borderColor = UIColor.clear.cgColor
    
            let flash = CASpringAnimation(keyPath: "borderColor")
            flash.damping = 7.0
            flash.stiffness = 200.0
            flash.fromValue = UIColor(red: 1.0, green: 0.27, blue: 0.0, alpha: 1.0).cgColor
            flash.duration = flash.settlingDuration
            textField.layer.add(flash, forKey: nil)
    
            textField.layer.cornerRadius = 5
    

    效果如下:

    CASpringTextField.gif

    5. CAKeyframeAnimation

    CABasicAnimation渐进式修改图层指定属性,在指定时间内从fromValue修改到toValue。例如,将图层从45度旋转到-45度,只需指定开始、结束值,layer 自动渲染中间状态以完成动画。

    CAKeyframeBasic.png

    CAKeyframeAnimation使用数组values取代fromValuebyValuetoValuevalues数组元素是动画需经过的点。此外,还需提供经过上述点的keyTimesCAKeyframeAnimation继承自CAPropertyAnimation,为图层对象提供关键帧(keyframe)动画。

    查看以下CAKeyframeAnimation动画:

    CAKeyframeExample.png

    上图中,动画从45度旋转至-45度,但分为两个阶段。第一阶段,前三分之二时间从45度旋转到22度,后三分之一时间从22度旋转至-45度。

    使用 keyframe 动画时,需提供属性的 key values,同时提供与之匹配数量的 key times,时间为相对比例,范围为0.0至1.0。

    5.1 常用属性

    CAKeyframeAnimation有以下常用属性:

    • values:数组类型,指定用于动画的keyframekeyframe指定了动画必须经过的位置,图层何时经过指定keyframe由动画时间函数决定,即calculationModekeyTimestimingFunctions属性等。keyframe之间的值自动插入,除非calculationMode被设置为了discrete。只有当path属性为nil时才会采用values属性的值。

    • pathCGPath类型,动画属性类型为CGPoint时,可以使用path指定点动画的路径。使用了path属性后,动画会忽略values属性。path可以包含move-to、line-to、curve-to等片段。

    • keyTimes:数组类型,可选实现。指定动画进行到指定keyframe的时间。数组元素为浮点值,范围为0.0至1.0,即指定动画进行到总持续时间百分比。后一个时间必须大于等于前一个时间。通常,keyTimes元素数量与valuespath元素数量应一致。否则,动画时间可能不符合预期。

      keyTimes数组元素值与calculationMode相关:

      • 如果calculationMode设置为linearcubic,则数组第一个元素需是0.0,最后一个元素需是1.0。中间部分值表示开始时间和结束时间之间的时间点。
      • 如果calculationMode设置为discrete,数组第一个元素必须是0.0,最后一个元素必须是1.0。keyTimes数组元素必须比values数组元素数量多一个。
      • 如果calculationMode设置为cubicPacedpaced,则自动忽略values数组。

      如果keyTimes数组对当前calculationMode无效或不合适,则会被自动忽略。

    • timingFunctions:数组类型,可选设置,数组元素为CAMediaTimingFunction类型。通过timingFunctions数组可以设置两个keyframe之间动画为淡入、淡出、自定义。如果values数组有n个元素,则该数组应包含n-1个元素。

      如果已经为keyTimes赋值,timingFunctions属性会对时间函数进一步优化。如果未设置keyTimes属性,则使用timingFunctions属性替换 Core Animation 默认时间函数。

    • calculationMode:指定CAKeyframeAnimation如何计算 keyframe 中间值。默认值为linear,即线性插入。此外,还有cubiccubicPaceddiscretelinearpaced

    • rotationMode:对象沿指定path运动过程中,绕切线旋转模式。默认为nil,即无需旋转。

    5.2 对非 struct 属性进行动画

    以下代码使用CAKeyframeAnimation晃动UILabel

            let wobble = CAKeyframeAnimation(keyPath: "transform.rotation")
            wobble.duration = 0.25
            wobble.repeatCount = 2
            wobble.values = [0.0, -.pi/4.0, 0.0, .pi/4.0, 0.0]
            wobble.keyTimes = [0.0, 0.25, 0.5, 0.75, 1.0]
            titleLabel.layer.add(wobble, forKey: nil)
    

    创建CAKeyframeAnimation与创建CABasicAnimation方式一致,指定keyPathdurationrepeatCount即可。

    旋转角度从0度到-45度,回归到0度,旋转到45度,回到0度。动画开始、结束位置相同,方便重复动画。确保keyTimes开始、结束分别是0.0、1.0。

    效果如下:

    CAKeyframeWobble.gif

    5.3 对 struct 属性进行动画

    结构体(struct)在 swift 中是一等公民,与 class 的使用几乎没有区别。

    但 Core Animation 是一个基于 C 的 Objective-C framework。这意味着,结构体的处理会有些不同。Objective-C API 喜欢处理对象,因此,struct 属性动画需要一些特殊处理。

    CALayer的很多属性是结构体,如positiontransformbounds等。为了解决这个问题,Cocoa 提供了NSValue类,用于将结构体包装为对象。NSValue提供了以下方法包装结构体:

    init(cgPoint: CGPoint)
    init(cgSize: CGSize)
    init(cgRect rect: CGRect)
    init(caTransform3D: CATransform3D)
    

    如果直接为fromValuetoValue赋值结构体,将无法得到预期的动画。

    首先,添加CALayer到视图:

            let balloon = CALayer()
            balloon.frame = CGRect(x: -50, y: 100, width: 50, height: 50)
            balloon.contents = UIImage(named: "balloon")!.cgImage
            view.layer.addSublayer(balloon)
    

    balloon图层放到了左上角的可见区域外。使用以下代码添加 keyframe 动画:

            let flight = CAKeyframeAnimation(keyPath: "position")
            flight.duration = 3.0
            flight.values = [
                CGPoint(x: -50.0, y: 0.0),
                CGPoint(x: view.bounds.width + 50, y: view.bounds.height / 2.0),
                CGPoint(x: -50.0, y: view.bounds.height - 100)
            ].map({ NSValue(cgPoint: $0) })
            flight.keyTimes = [0.0, 0.5, 1.0]
            balloon.add(flight, forKey: nil)
            balloon.position = CGPoint(x: -50.0, y: view.bounds.height - 100)
    

    动画经过了三个指定点。运行后效果如下:

    CAKeyframeBalloon.gif

    还可使用上述方法对boundspositiontransform等结构体添加动画。

    6. CATransition

    CATransition对象提供 layer 状态切换的动画。CATransition继承自CAAnimation

    默认 transition 是交叉淡入淡出(cross fade)。通过创建、添加CATransition对象,可以选取不同 transition 效果。

    6.1 常用属性

    CATransition有以下常用属性:

    • startProgress:指定动画起点处于整个 transition 的百分比。Float 类型,值范围是0.0至1.0,默认为0。

    • endProgress:指定动画终点处于整个 transition 的百分比。Float 类型,值范围是0.0至1.0,默认为0。endProgress值必须大于等于startProgress,并不大于1.0。如果endProgress小于startProgress,则结果不可预期。默认值为1.0。

    • type:预定义的 transition type,为CATransitionType类型。如果设置了filter属性,则会忽略type属性。

      CATransitionType有以下类型:

      • fade:淡入淡出,默认属性。
      • moveIn:图层内容在现有内容之上滑入。moveInsubType一起使用。
      • push:图层内容推动着现有内容进入。pushsubType一起使用。
      • reveal:图层内容根据subType指定方向逐渐显示。
    • subType:指定 transition 方向。CATransitionSubtype类型,默认为nil。如果设置了filter属性,则会忽略subType属性。

      CATransitionSubtype有以下类型:

      • fromBottom:transition 从图层底部开始。
      • fromLeft:transition 从图层左侧开始。
      • fromRight:transition 从图层右侧开始。
      • fromTop:transition 从图层顶部开始。
    • filter:可选 Core Image Filter 对象提供 transition。只可用于macOS和 Mac Catalyst 13.0,不可用于 iOS。

    6.2 对 CATextLayer 添加 transition

    下面代码演示了如何过渡CATextLayer。过渡前,backgroundColor为红色,string为Red。过渡时,创建了一个新的CATransition并添加到图层,图层背景色过渡为蓝色,文本内容过渡为Blue。代码如下:

            let transition = CATransition()
            transition.duration = 2
            transition.type = .push
            transitioningLayer.add(transition, forKey: nil)
            
            // Transition to "blue" state
            transitioningLayer.backgroundColor = UIColor.blue.cgColor
            transitioningLayer.string = "Blue"
    

    效果如下:

    CATransition.gif

    6.3 对 NavigationController 添加 transition

    这一部分使用CATransition为控制器之间导航添加过渡动画。

    首先为UINavigationController添加以下 extension:

    extension UINavigationController {
        func pushTransition() {
            let transition = CATransition()
            transition.duration = 1.0
            transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
            transition.type = .push
            transition.subtype = .fromTop
            view.layer.add(transition, forKey: nil)
        }
        
        func popTransition() {
            let transition = CATransition()
            transition.duration = 1.0
            transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
            transition.type = .push
            transition.subtype = .fromBottom
            view.layer.add(transition, forKey: nil)
        }
    }
    

    可以尝试不同typesubType,查看不同效果。

    当执行push、pop时,先调用pushTransition()popTransition()函数。如下所示:

        @objc private func handlePushButtonTapped() {
            navigationController?.pushTransition()
            
            let explicitlyVC = ExplicitlyViewController()
            navigationController?.pushViewController(explicitlyVC, animated: false)
        }
        
        @objc private func handlePopButtonTapped() {
            navigationController?.popTransition()
            
            navigationController?.popViewController(animated: false)
        }
    

    效果如下:

    CATransitionNav.gif

    如果是 present 视图控制器,可以使用modalTransitionStyle修改呈现方式。

    7. CAAnimationGroup

    CAAnimationGroup允许多个动画在一个组中同时运行。CAAnimationGroup继承自CAAnimation

    添加到 animation group 的动画,它的时间不会缩放到CAAnimationGroupduration,而是将超出CAAnimationGroupduration部分直接裁剪掉。例如,CAAnimationGroup动画duration时间为5秒,添加到该 group 动画duration为10秒,则仅显示前5秒钟动画。

    下面代码将改变不透明度、缩放、旋转三个动画添加到CAAnimationGroup

            let fadeOut = CABasicAnimation(keyPath: "opacity")
            fadeOut.fromValue = 0
            fadeOut.toValue = 1
            
            let expandScale = CABasicAnimation(keyPath: "transform")
            expandScale.valueFunction = CAValueFunction(name: .scale)
            expandScale.fromValue = [1, 1, 1]
            expandScale.toValue = [1.5, 1.5, 1.5]
            
            let rotate = CABasicAnimation(keyPath: "transform")
            rotate.valueFunction = CAValueFunction(name: .rotateZ)
            rotate.fromValue = Float.pi / 4.0
            rotate.toValue = 0.0
            
            let group = CAAnimationGroup()
            group.animations = [fadeOut, expandScale, rotate]
            group.duration = 0.5
            group.beginTime = CACurrentMediaTime() + 0.5
            group.fillMode = .backwards
            group.delegate = self
            
            layerView.layer.add(group, forKey: nil)
    

    添加到CAAnimationGroup的动画将忽略delegateisCompletedOnCompletion属性,CAAnimationGroup会接收这些信息。

    效果如下:

    CAAnimationGroup.gif

    8. CAAnimationDelegate

    CAAnimationDelegate.png

    CAAnimation遵守了CAAnimationDelegate协议。CAAnimationDelegate协议包含了下面两个可选实现的方法:

    • optional func animationDidStart(_ anim: CAAnimation)动画开始时调用。
    • optional func animationDidStop(_ anim: CAAnimation, finished flag: Bool)动画结束时调用。动画结束可能是因为到达duration,也可能是因为添加动画的图层被移除。如果是达到指定duration,flag为true;反之,flag为false。

    CAAnimationGroup添加delegate

            group.delegate = self
    

    添加以下 delegate 方法:

    extension ExplicitlyViewController: CAAnimationDelegate {
        func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
            print(#function)
        }
    }
    

    动画结束后会输出以下内容:

    animationDidStop(_:finished:)
    

    实际应用时可能有多个动画同时遵守CAAnimationDelegate协议,但如何区分是哪个动画结束调用的CAAnimationDelegate方法呢?

    8.1 键值编码 Key-value coding compliance

    CAAnimation类和子类都是使用 Objective-C 编写的,都遵守KVC。也就是可以像字典一样在运行时添加属性。

    我们将使用KVO机制为动画添加属性,以便在需要时可以对动画进行区分。

            let flyRight = CABasicAnimation(keyPath: "position.x")
            flyRight.fromValue = -view.bounds.size.width/2
            flyRight.toValue = view.bounds.size.width/2
            flyRight.duration = 2
            flyRight.delegate = self
            flyRight.setValue("form", forKey: "name")
            flyRight.setValue(titleLabel.layer, forKey: "layer")
            titleLabel.layer.add(flyRight, forKey: "title")
            
            flyRight.beginTime = CACurrentMediaTime() + 0.3
            flyRight.fillMode = .both
            flyRight.setValue(textField.layer, forKey: "layer")
            textField.layer.add(flyRight, forKey: "field")
            
            titleLabel.layer.position.x = view.bounds.size.width / 2
            textField.layer.position.x = view.bounds.size.width / 2
    

    在上述代码中,添加了键form,值为name。这样可以在CAAnimationDelegate回掉方法中根据form区分动画。

    Core Animation 动画对象只是数据模型,创建后只需修改所需属性即可。

    CABasicAnimation实例描述了动画,可以现在执行、稍后执行,也可以不执行。动画不关联特定 layer,可以复用 animation 到其他 layer,每个 layer 都将获得单独的 animation,

    8.2 animationDidStop(_:finished:)

    目前已经为动画设置了 key,在animationDidStop(_:finished:)方法中添加以下代码:

        func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
            print(#function)
            
            if !flag {
                print("Did not reached the end of the duration")
                return
            }
            
            guard let name = anim.value(forKey: "name") as? String else { return }
            
            if name == "form" { // form field found
                let layer = anim.value(forKey: "layer") as? CALayer
                anim.setValue(nil, forKey: "layer")
                
                let pulse = CABasicAnimation(keyPath: "transform.scale")
                pulse.fromValue = 1.5
                pulse.toValue = 1.0
                pulse.duration = 0.25
                layer?.add(pulse, forKey: nil)
            }
        }
    

    如果动画不是因为到达duration结束,则不再添加动画。使用value(forKey:)从动画中获取值,并转为String类型。

    value(forKey:)返回值类型是AnyObject?,需转换为所需类型,且转换可能失败。

    最终,当动画结束后添加了一个放大动画。

    8.3 Animation Keys

    add(_:forKey:)函数有以下两个参数:

    • anim:第一个参数是要添加到 render tree 的动画。render tree 对该参数进行复制、而非引用。因此,随后对动画的修改不会改变 render tree 中的动画。
    • key:标记该动画的 key。每个单独 key 只添加一个动画到图层,对于 transition animation 会自动使用kCATransition特殊key。该参数可以为nil。使用 key 可以在动画开始后获取、管理动画。

    如果动画duration为0或负值,则duration会被设置为当前的kCATransactionAnimationDuration(如果设置了该值),或采用动画默认时长,即0.25秒。

    在上面的键值编码部分,为titleLabel设置的key是title,为textField设置的key是field。这里使用以下代码移除运行中的动画:

            titleLabel.layer.removeAnimation(forKey: "title")
            textField.layer.removeAnimation(forKey: "field")
    

    当titleLabel、textField向右移动过程中,点击chang按钮时调用上述方法,会立即移除动画。效果如下:

    CAAnimationKey.gif

    Demo名称:CoreAnimation
    源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/CoreAnimation

    上一篇:CALayer及其各种子类

    下一篇:图层时间CAMediaTiming

    参考资料:

    1. Animations
    2. Core Animation

    欢迎更多指正:https://github.com/pro648/tips

    本文地址:https://github.com/pro648/tips/blob/master/sources/CAAnimation:属性动画CABasicAnimation、CAKeyframeAnimation以及过渡动画、动画组.md

    相关文章

      网友评论

        本文标题:CAAnimation:属性动画CABasicAnimation

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