本文是在学习《iOS Animations by Tutorials》一书后,对其中一些比较重要部分做了摘录而来。
Section II View Animations
这个 Section 主要会介绍一下 UIKit 动画。
3 Getting started with view animations
允许做 UIKit 动画的属性
这块可动画的属性,比较常见:
- frame bounds size
- backgroundColor alpha
- transform
Option 设置
如何做到动画有来有回:[.repeat, .autoreverse]
4 Springs
弹簧动画
关键参数解释
- damping:摩擦力,摩擦力越大,动画越快停止
- 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
如何实现慢速动画
有时候需要能够对动画进行慢速调试,从而看清楚整个动画过程,该怎么办呢?
- 模拟器菜单的 slow animation
- lldb 里设置 layer.speed
- 代码里直接设置
过渡动画,从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 的不同:
- Layer 只是一个 model object,没有自动布局和用户交互的功能
- 可以额外设置边界、边界颜色、阴影、圆角等
- 直接通过 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
四个重要属性
- damping:摩擦力
- mass:物体质量
- stiffness:重力加速度、引力
- initalVelocity:起始速度
如何理解这四个参数:
摆钟在左右荡,damping 就是空气摩擦力。如果 damping 为 0,就会一直摆动,不停止。摆钟越重 mass 越大,摆的时间越长。地球和月球的引力(stiffness)是不同的。起始速度(initalVelocity)决定刚开始摆动的速度
和 UIKit Spring 动画的区别
- UIKit 动画需要自己给定动画时长,因此如果和设置的参数不配套,会给人一种突然停止动画的感觉
- 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 的关键帧动画不同点
- UIKit 的动画,中间允许有空隙,可以针对不同视图进行动画
- 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
类允许绘制多种多样的图形。比如下面的图形:
![](https://img.haomeiwen.com/i2596934/2a3ed03c76876b64.png)
Mask 概念如下,只需要 mask 的轮廓:
![](https://img.haomeiwen.com/i2596934/b45decd962cb373e.png)
参考代码
// 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
![](https://img.haomeiwen.com/i2596934/f9812e82057b1c19.png)
实现方法如下:
- 创建 CAGradientLayer,并设置渐变
- 根据 text 生成 UIImage,并将 UIImage 设置为 CAGradientLayer 的蒙版
- 创建动画 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 还允许设置动画的属性包括:
- colors
- startPoint
- endPoint
- locations
17 Stroke & Path Animation
18 Replicating Animations
CAReplicatorLayer
可以实现动画的复制:
![](https://img.haomeiwen.com/i2596934/b1513c79de6e3424.png)
实现代码
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)
可以动画的属性包括:
- instanceDelay
- instanceTransform
- instanceColor
- instanceRedOffset/instanceGreenOffset/instanceBlueOffset
- 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)
网友评论