美文网首页
《iOS Animations by Tutorials》读书笔

《iOS Animations by Tutorials》读书笔

作者: 郑一一一一 | 来源:发表于2020-03-31 14:14 被阅读0次

本文是在学习《iOS Animations by Tutorials》一书后,对其中一些比较重要部分做了摘录而来。

Section II View Animations

这个 Section 主要会介绍一下 UIKit 动画。

3 Getting started with view animations

允许做 UIKit 动画的属性

这块可动画的属性,比较常见:

  1. frame bounds size
  2. backgroundColor alpha
  3. transform

Option 设置

如何做到动画有来有回:[.repeat, .autoreverse]

4 Springs

弹簧动画

关键参数解释

  1. damping:摩擦力,摩擦力越大,动画越快停止
  2. velocity:起始速度
UIView.animate(withDuration: 0.5, 
             delay: 0.5, 
             usingSpringWithDamping: 0.5,
             initialSpringVelocity: 0.0, 
             options: [], animations: {
  self.loginButton.center.y -= 30.0
  self.loginButton.alpha = 1.0
}, completion: nil)

5 Transitions

如何实现慢速动画

有时候需要能够对动画进行慢速调试,从而看清楚整个动画过程,该怎么办呢?

  1. 模拟器菜单的 slow animation
  2. lldb 里设置 layer.speed
  3. 代码里直接设置

过渡动画,从oldView 过渡到 newView,示例代码:

//replace via transition
UIView.transition(from: oldView, to: newView, duration: 0.33, 
  options: .transitionFlipFromTop, completion: nil)

7 Keyframe Animations

UIKit 关键帧动画:可以在一个时间范围内,针对不同视图,不同属性做动画。

UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: [.calculationModeCubic], animations: {
    // withRelativeStartTime relativeDuration 都是相对时长,不能超过 1
    UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25,
                       animations: {
                        self.planeImage.center.x += 80.0
                        self.planeImage.center.y -= 10.0
    }
    )
    
    // 允许不同动画之间有交叉
    UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.4) {
        self.planeImage.transform = CGAffineTransform(rotationAngle: -.pi / 8)
    }

    UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25) {
        self.planeImage.center.x += 100.0
        self.planeImage.center.y -= 50.0
        self.planeImage.alpha = 0.0
    }

    UIView.addKeyframe(withRelativeStartTime: 0.51, relativeDuration: 0.01) {
      self.planeImage.transform = .identity
      self.planeImage.center = CGPoint(x: 0.0, y: originalCenter.y)
    }

    UIView.addKeyframe(withRelativeStartTime: 0.55, relativeDuration: 0.45) {
      self.planeImage.alpha = 1.0
      self.planeImage.center = originalCenter
    }
}, completion: { _ in

})

9 Animating Constraints

针对约束,做动画的方式:

// 修改约束,甚至可以不用写在 animation block 中
menuHeightConstraint.constant = isMenuOpen ? 184.0 : 44.0

UIView.animate(withDuration: 1.0, delay: 0.0, 
  usingSpringWithDamping: 0.4, initialSpringVelocity: 10.0, 
  options: .curveEaseIn, 
  animations: {
    // 关键代码 只要把这个放在 animation 里即可
    // 确保马上会调用 layoutSubviews 方法,从而计算最新的坐标,而不是等到下个更新周期
    self.view.layoutIfNeeded()
  }, 
  completion: nil
)

如果想要修改除了 constant 之外约束的其他参数,则需要重新创建,创建 Layout 的代码:

// 以下代码可以翻译为公式:titleLabel.centerY = titleLabel.superview.centerY * isMenuOpen ? 0.67 : 1.0 + 10
let newConstraint = NSLayoutConstraint(
                                    item: titleLabel,
                                    attribute: .centerY,
                                    relatedBy: .equal,
                                    toItem: titleLabel.superview,
                                    attribute: .centerY,
                                    multiplier: isMenuOpen ? 0.67 : 1.0,
                                    constant: 10)
// 唯一标识符,可以用于查找约束
newConstraint.identifier = "TitleCenterY"
// 令约束生效
newConstraint.isActive = true

Section III Layer Animations

这个 Section 主要介绍 CALayer 动画。

CALayer 与 UIView 的不同:

  1. Layer 只是一个 model object,没有自动布局和用户交互的功能
  2. 可以额外设置边界、边界颜色、阴影、圆角等
  3. 直接通过 GPU 来优化内容缓存和快速绘制(UIView 是通过 CPU)

10 Getting Started with Layer Animation

最基本的 CALayer 动画,CABasicAnimation

// 修改 X 坐标
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5
// 实现 delay
flyRight.beginTime = CACurrentMediaTime() + 0.3
// 添加动画,立即开始动画
heading.layer.add(flyRight, forKey: nil)

注意,动画和真实内容的区别:动画过程中,内容只是一个呈现树,并不是真正的控件。

// 如何在动画结束清除动画内容? 默认为 true
flyRight.isRemovedOnCompletion = true

11 Animation Keys & Delegate

设置代理

CA 同样可以在动画开始或结束时,得到回调通知。

// 设置代理
animation.delegate = self
// 常用代理方法
func animationDidStart(_ anim: CAAnimation)
func animationDidStop(_ anim: CAAnimation, finished flag: Bool)

Animation 支持 KVC,可以携带需要的上下文信息

// set
animation.setValue(username.layer, forKey: "layer")
// get
guard let layer = anim.value(forKey: "layer") as? CALayer else {
  return
}

如何辨别 CAAnimation

// 为 animation 加 key,用 key 做唯一标识符
info.layer.add(fadeLabelIn, forKey: "fadein")
// 获取 layer 所有动画 key
guard let runningAnimations = info.layer.animationKeys()

12 Groups & Advanced Timing

如何做一周半的动画

animation.autoreverse = true
animation.repeatCount = 2.5

修改动画的整体速度

// 允许速度叠加
animation.speed = 2.0
info.layer.speed = 2.0
view.layer.speed = 2.0

动画时间曲线

// 使用系统预定义的时间函数 option
groupAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)

// 同样可以采用控制点的方式来自定义曲线
CAMediaTimingFunction(controlPoints: _: _: _:)

group 组动画的实现

let groupAnimation = CAAnimationGroup()
groupAnimation.beginTime = CACurrentMediaTime() + 0.5
groupAnimation.duration = 0.5
groupAnimation.fillMode = .backwards

let scaleDown = CABasicAnimation(keyPath: "transform.scale")
scaleDown.fromValue = 3.5
scaleDown.toValue = 1.0

let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = .pi / 4.0
rotate.toValue = 0.0

// 组动画,综合在一块
groupAnimation.animations = [scaleDown, rotate]
loginButton.layer.add(groupAnimation, forKey: nil)

13 Layer Springs

四个重要属性

  1. damping:摩擦力
  2. mass:物体质量
  3. stiffness:重力加速度、引力
  4. initalVelocity:起始速度

如何理解这四个参数:

摆钟在左右荡,damping 就是空气摩擦力。如果 damping 为 0,就会一直摆动,不停止。摆钟越重 mass 越大,摆的时间越长。地球和月球的引力(stiffness)是不同的。起始速度(initalVelocity)决定刚开始摆动的速度

和 UIKit Spring 动画的区别

  1. UIKit 动画需要自己给定动画时长,因此如果和设置的参数不配套,会给人一种突然停止动画的感觉
  2. CA 可以通过参数计算出动画所需时长,动画表现形式更加自然,设置完参数以后,可以通过 settlingDuration 属性得到比较适宜的动画时长。

参考代码

let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.25
pulse.toValue = 1.0
// 大于 0 默认值 10
pulse.damping = 2.0
// 默认值 0 如果是负数,说明是在反方向运动
pulse.initialVelocity = 100
// 必须大于 0,默认值 1,值越大,重量越大
pulse.mass = 10
// 必须大于 0,默认值是 100
pulse.stiffness = 1500
// 可以通过上述参数计算出所需时长
pulse.duration = pulse.settlingDuration
layer?.addAnimation(pulse, forKey: nil)

14 Layer Keyframe Animation & Struct Properties

CA 和 UIKit 的关键帧动画不同点

  1. UIKit 的动画,中间允许有空隙,可以针对不同视图进行动画
  2. CA 只能允许对给定的一个 layer 的某个属性进行动画,动画之间不允许有间隔,个人感觉,可以说和 UIKit 完全是不同的东西

结构体参数如何做动画

比如 CGPoint、CGRect 不能直接设置 fromValue 参数。需要使用 NSValue 进行一层封装

move.fromValue = NSValue(cgPoint: CGPoint(x: 100.0, y: 100.0))

参考代码

let flight = CAKeyframeAnimation(keyPath: "position")
flight.duration = 12.0

// 从设置不同时间点,参数值
flight.values = [
  CGPoint(x: -50.0, y: 0.0),
  CGPoint(x: view.frame.width + 50.0, y: 160.0),
  CGPoint(x: -50.0, y: loginButton.center.y)
].map { NSValue(cgPoint: $0) }
// 设置对应的时间点
flight.keyTimes = [0.0, 0.5, 1.0]

balloon.add(flight, forKey: nil)

15 Shapes & masks

CAShaperLayer 类允许绘制多种多样的图形。比如下面的图形:

Mask 概念如下,只需要 mask 的轮廓:


mask 概念

参考代码

// CAShapeLayer 使用
let circleLayer = CAShapeLayer()
// 使用 UIBezierPath 绘制路径
circleLayer.path = UIBezierPath(ovalIn: bounds).cgPath
// 外围线的颜色
circleLayer.strokeColor = UIColor.white.cgColor
// 线框
circleLayer.lineWidth = lineWidth
// 填充颜色
circleLayer.fillColor = UIColor.clear.cgColor

// mask 概念 给轮廓
let maskLayer = CAShapeLayer()
maskLayer.path = circleLayer.path
maskLayer.position = CGPoint(x: 0.0, y: 10.0)
circleLayer.mask = maskLayer

Path 动画

设置按特定路径的动画,同样可以使用贝塞尔曲线 UIBezierPath。

// Path
let morphAnimation = CABasicAnimation(keyPath: "path")

morphAnimation.duration = animationDuration
morphAnimation.toValue = UIBezierPath(ovalIn: morphedFrame).cgPath
morphAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)

layer.add(morphAnimation, forKey: nil)

16 Gradient Animations

CAGradientLayer 类可以实现图层的渐变色。

在此基础上,可以实现 iOS 经典的渐变效果 Slide to unlock

渐变效果

实现方法如下:

  1. 创建 CAGradientLayer,并设置渐变
  2. 根据 text 生成 UIImage,并将 UIImage 设置为 CAGradientLayer 的蒙版
  3. 创建动画 CABasicAnimation, keyPath 设置为 locations,并设置 fromValue,toValue
import UIKit
import QuartzCore

class AnimatedMaskLabel: UIView {
    /// 渐变 layer
    let gradientLayer: CAGradientLayer = {
        let gradientLayer = CAGradientLayer()

        // Configure the gradient here
        gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
        gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)

        let colors = [UIColor.black.cgColor,
                      UIColor.white.cgColor,
                      UIColor.black.cgColor]
        gradientLayer.colors = colors

        let locations: [NSNumber] = [
            0.25,
            0.5,
            0.75
        ]
        gradientLayer.locations = locations

        return gradientLayer
    }()
    
    // 字体属性
    let textAttributes: [NSAttributedString.Key: Any] = {
        let style = NSMutableParagraphStyle()
        style.alignment = .center
        return [
            .font: UIFont(
                name: "HelveticaNeue-Thin",
                size: 28.0)!,
            .paragraphStyle: style
        ]
    }()
    
    // text 创建蒙版
    @IBInspectable var text: String! {
        didSet {
            setNeedsDisplay()
            // 通过文字生成照片
            let image = UIGraphicsImageRenderer(size: bounds.size).image { _ in
                text.draw(in: bounds, withAttributes: textAttributes)
            }
            
            let maskLayer = CALayer()
            maskLayer.backgroundColor = UIColor.clear.cgColor
            maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
            maskLayer.contents = image.cgImage

            gradientLayer.mask = maskLayer
        }
    }

    override func layoutSubviews() {
        layer.borderColor = UIColor.green.cgColor

//        gradientLayer.frame = bounds
        gradientLayer.frame = CGRect(x: -bounds.size.width,
                                     y: bounds.origin.y,
                                     width: 3 * bounds.size.width,
                                     height: bounds.size.height)
    }

    override func didMoveToWindow() {
        super.didMoveToWindow()
        layer.addSublayer(gradientLayer)
        
        // 执行动画
        // locations
        let gradientAnimation = CABasicAnimation(keyPath: "locations")
        gradientAnimation.fromValue = [0, 0, 0.25]
        gradientAnimation.toValue = [0.75, 1.0, 1.0]

        // 动画是 colors
//        let gradientAnimation = CABasicAnimation(keyPath: "colors")
//        gradientAnimation.fromValue = [UIColor.yellow.cgColor, UIColor.black.cgColor, UIColor.blue.cgColor]
//          gradientAnimation.toValue = [UIColor.white.cgColor, UIColor.yellow.cgColor, UIColor.black.cgColor]

        gradientAnimation.duration = 3.0
        gradientAnimation.repeatCount = Float.infinity

        gradientLayer.add(gradientAnimation, forKey: nil)
    }

}

此外,CAGradientLayer 还允许设置动画的属性包括:

  1. colors
  2. startPoint
  3. endPoint
  4. locations

17 Stroke & Path Animation

18 Replicating Animations

CAReplicatorLayer 可以实现动画的复制:

实现代码

let replicator = CAReplicatorLayer()
let dot = CALayer()
replicator.frame = view.bounds
view.layer.addSublayer(replicator)

dot.frame = CGRect(x: replicator.frame.size.width - dotLength,
                   y: replicator.position.y,
                   width: dotLength,
                   height: dotLength)
dot.backgroundColor = UIColor.lightGray.cgColor
dot.borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor
dot.borderWidth = 0.5
dot.cornerRadius = 1.5
replicator.addSublayer(dot)
// 设置复制的个数
replicator.instanceCount = Int(view.frame.size.width / dotOffset)
// 与上一个相比复制图层的 transform
replicator.instanceTransform = CATransform3DMakeTranslation(-dotOffset, 0, 0)

可以动画的属性包括:

  1. instanceDelay
  2. instanceTransform
  3. instanceColor
  4. instanceRedOffset/instanceGreenOffset/instanceBlueOffset
  5. instanceAlphaOffset

Section V: View Controller Transition Animations

这个 Section 主要会介绍 ViewController 的过渡动画。

19 Presentation Controller & Orientation Animations

tip:layer.cornerRadius 也可以用 UIKit 进行渐变动画

在 present 时,如何实现过渡动画的自定义:

class ViewController: UIViewController {
    // 要点 1:遵循 UIViewControllerAnimatedTransitioning 协议的自定义类
    let transition = PopAnimator()
 
    //MARK: Actions

    @objc func didTapImageView(_ tap: UITapGestureRecognizer) {
        // 创建 vc 跳转
        let herbDetails = HerbDetailsViewController()
        herbDetails.herb = selectedHerb
        herbDetails.modalPresentationStyle = .overFullScreen
        // 要点 3:设置 transition 代理
        herbDetails.transitioningDelegate = self
        // 执行 present
        present(herbDetails, animated: true, completion: nil)
    }
}

extension ViewController: UIViewControllerTransitioningDelegate {
    // 要点 4:present 时的回调代理
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        transition.presenting = true

        return transition
    }
    
    // 要点 5:dimiss 回调代理
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        transition.presenting = false
        return transition
    }

    /// 提示 vc.view 尺寸即将发生变化,比如旋转时,进行自适应
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        coordinator.animate(alongsideTransition: { context in
            self.bgImage.alpha = (size.width>size.height) ? 0.25 : 0.55
        }, completion: nil)

        self.positionListItems()
    }
}

PopAnimator 类定义:

// 要点:继承和遵循协议看一下
class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    var presenting = true
    
    // 要点:设置过渡时长
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2
    }
    // 要点:设置具体的过渡动画
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
       
        // 获取父 view
        let containerView = transitionContext.containerView
        // 要点:通过 context 获取 view
        let toView = transitionContext.view(forKey: .to)
        let fromView = transitionContext.view(forKey: .from)
        
        // 获取 vc
        let toVC = transitionContext.viewController(forKey: .to)
        let fromVC = transitionContext.viewController(forKey: .from)
        
        // 设置 CA 或者 UIKit 动画
        
        // 要点:动画全部结束,需要调用这句代码,告诉系统动画完成啦
        transitionContext.completeTransition(true)
    }
}

19 UINavigationController Custom Transition Animations

重写导航栏 push 动画的方式与 present 类似,代码如下:


// 遵循 UINavigationControllerDelegate 代理,并实现 animationControllerFor 代理方法
extension XXX: UINavigationControllerDelegate {
    /// 无交互的自定义过渡代理方法
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        // 自定义类
        transition.operation = operation
        return transition
    }
}

class RevealAnimator: NSObject, UIViewControllerAnimatedTransitioning, CAAnimationDelegate {
    let animationDuration = 2.0
    var operation: UINavigationController.Operation = .push

    // 用于存储 layer animation 的上下文
    weak var storedContext: UIViewControllerContextTransitioning?
    
    /// 定义动画时长
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return animationDuration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // 执行动画
        
        // 通知系统动画完成
        transitionContext.completeTransition(true)
    }
}

21 Interactive UINavigationController Transitions

交互式过渡动画支持根据手势的偏移量,进行过渡动画,其实现代码如下:

需要在 View Controller 中定义手势,来获取偏移量

// 必须继承 UIPercentDrivenInteractiveTransition
class RevealAnimator: UIPercentDrivenInteractiveTransition {
    var interactive = false
    
    // 在外部传入 pan 手势,根据 pan 属性,决定 transition 进度
    func handlePan(_ recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translation(in: recognizer.view!.superview!)
        var progress: CGFloat = abs(translation.x / 200)
        // 0.01 0.99 是经验所得
        progress = min(max(progress, 0.01), 0.99)

        switch recognizer.state {
        case .changed:
            // 类自带的方法 更新进度
            update(progress)
        case .cancelled, .ended:
            if progress < 0.5 {
                // 取消
                cancel()
            } else {
                // 结束
                finish()
            }
            interactive = false
        default:
            break
        }
    }

}

Section VI:Animations with UIViewPropertyAnimator

UIViewPropertyAnimator 类在 iOS 10 中引入,最大特点是支持交互式、可打断的动画。

22 Get started with UIViewPropertyAnimator

一般用法

let scale = UIViewPropertyAnimator(duration: 0.3, curve: .easeIn, animations: nil)

// delayFactor 是相对于 remainDuration 的相对时长,为什么不要用绝对时长?因为可以允许中途中断
scale.addAnimations({
  view.alpha = 1.0
}, delayFactor: 0.33)

// 可以添加多个 completion
scale.addCompletion { _ in
  print("ready 1")
}

scale.addCompletion { _ in
  print("ready 2")
}

scale.startAnimation()

如何嵌套使用 Key Frame 动画

static func jiggle(view: UIView) -> UIViewPropertyAnimator {

  // UIViewPropertyAnimator animations 里可以加 UIView.animateKeyframes,这样子就可以实现嵌套啦
  return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.33, delay: 0, options: [], animations: {
    // 注意里面嵌套了 UIKit animateKeyframes 动画
    UIView.animateKeyframes(withDuration: 1, delay: 0, options: [], animations: {
      UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.25) {
        view.transform = CGAffineTransform(rotationAngle: -.pi/8)
      }

      UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75) {
        view.transform = CGAffineTransform(rotationAngle: +.pi/8)
      }

      UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 1) {
        view.transform = CGAffineTransform.identity
      }
    }, completion: nil)

  }, completion: { _ in
    view.transform = .identity
  })
}

23 Intermediate Animations with UIViewPropertyAnimator

实现自定义曲线

// 传入控制点,实现自定义曲线
UIViewPropertyAnimator(duration: 0.55,
                       controlPoint1: CGPoint(x: 0.57, y: -0.4),
                       controlPoint2: CGPoint(x: 0.96, y: 0.87),
                       animations: blurAnimations(blurred))
  .startAnimation()

Spring

UIViewPropertyAnimator(duration: 0.3, dampingRatio: 0.5) {
      //animations
}

自定义 timingParameters

let spring = UISpringTimingParameters(dampingRatio: 0.5, initialVelocity: CGVector(dx: 1.0, dy: 0.2))
let spring1 = UISpringTimingParameters(mass: 10, stiffness: 10, damping: 10, initialVelocity: CGVector(dx: 1.0, dy: 0.2))
let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring)

相关文章

网友评论

      本文标题:《iOS Animations by Tutorials》读书笔

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