使用 UIKit 和 UIView 在视图上执行 iOS 动画
已发表: 2022-03-10十多年来,我一直是一名 iOS 开发人员,很少看到文章整合了在 iOS 中执行动画的所有可能方式。 本文旨在成为 iOS 动画的入门读物,旨在详尽地介绍实现相同效果的不同方式。
鉴于该主题的广泛性,我们将在相当高的水平上简洁地介绍每个部分。 目标是通过一组选择来教育读者将动画添加到他/她的 iOS 应用程序。
在开始讨论与 iOS 相关的主题之前,让我们简要了解一下动画速度。
60FPS 动画
通常在视频中,每一帧都由一个图像表示,帧速率决定了序列中翻转的图像数量。 这被称为“每秒帧数”或 FPS。
FPS决定了一秒内翻转的静止图像的数量,字面意思是图像/帧的数量越多,视频中显示的细节/信息就越多。 这也适用于动画。
FPS 通常用于确定动画的质量。 有一种流行的观点认为,任何好的动画都应该以 60fps 或更高的速度运行——任何低于 60fps 的动画都会让人觉得有点过时。
你想看看 30FPS 和 60FPS 的区别吗? 检查这个!
你注意到区别了吗? 人眼绝对可以在较低的 fps 下感觉到抖动。 因此,确保您创建的任何动画都遵循以 60 FPS 或更高速度运行的基本规则始终是一个好习惯。 这使它感觉更加真实和生动。
看过 FPS 之后,现在让我们深入研究不同的核心 iOS 框架,它们为我们提供了一种执行动画的方式。
核心框架
在本节中,我们将介绍 iOS SDK 中可用于创建视图动画的框架。 我们将快速浏览它们中的每一个,并通过相关示例解释它们的功能集。
UIKit/ UIView 动画
UIView 是在 iOS 应用程序中显示内容的任何视图的基类。
UIKit,这个给我们提供 UIView 的框架,已经为我们提供了一些基本的动画功能,方便开发者事半功倍。
API UIView.animate
是动画视图的最简单方法,因为任何视图的属性都可以通过在基于块的语法中提供属性值来轻松动画。
在 UIKit 动画中,建议只修改 UIVIew 的动画属性,否则动画可能会导致视图最终处于意外状态。
动画(withDuration:动画:完成)
这个方法接受动画持续时间,一组需要动画的视图的动画属性变化。 当视图完成执行动画时,完成块会给出回调。
几乎任何类型的动画,如视图上的移动、缩放、旋转、褪色等,都可以使用这个单一的 API 来实现。
现在,考虑您想要为按钮大小更改设置动画,或者您想要将特定视图放大到屏幕中。 这就是我们使用UIView.animate
API 的方法:
let newButtonWidth: CGFloat = 60 UIView.animate(withDuration: 2.0) { //1 self.button.frame = CGRect(x: 0, y: 0, width: newButtonWidth, height: newButtonWidth) //2 self.button.center = self.view.center //3 }
这是我们在这里所做的:
- 我们调用
UIView.animate
方法,传递给它的持续时间值表示块内描述的动画应该运行多长时间。 - 我们设置了应该代表动画最终状态的按钮的新帧。
- 我们将按钮
center
设置为其超级视图的中心,以便它保持在屏幕的中心。
上面的动画代码块应该触发按钮帧从当前帧改变的动画:
Width = 0, Height = 0
到最后一帧:
Width = Height = newButtonWidth
这是动画的样子:
animateWithDuration
此方法类似于 animate 方法的扩展,您可以在其中执行您可以在先前的 API 中执行的所有操作,并将一些物理行为添加到视图动画中。
例如,如果您想在我们上面所做的动画中实现弹簧阻尼效果,那么代码如下所示:
let newButtonWidth: CGFloat = 60 UIView.animate(withDuration: 1.0, //1 delay: 0.0, //2 usingSpringWithDamping: 0.3, //3 initialSpringVelocity: 1, //4 options: UIView.AnimationOptions.curveEaseInOut, //5 animations: ({ //6 self.button.frame = CGRect(x: 0, y: 0, width: newButtonWidth, height: newButtonWidth) self.button.center = self.view.center }), completion: nil)
这是我们使用的一组参数:
-
duration
表示动画的持续时间,确定代码块应该运行多长时间。 -
delay
表示我们希望在动画开始之前具有的初始延迟。 -
SpringWithDamping
表示我们希望视图表现的弹性效果的值。 该值必须在 0 到 1 之间。该值越低,弹簧振动越高。 -
velocity
表示动画应该开始的速度。 -
options
要应用于视图动画的动画曲线类型。 - 最后,我们设置需要动画的按钮框架的代码块。 和之前的动画一样。
以下是使用上述动画配置的动画效果:
UIViewPropertyAnimator
为了更好地控制动画, UIViewPropertyAnimator
很方便,它为我们提供了一种暂停和恢复动画的方法。 你可以有自定义的时间,让你的动画是交互式的和可中断的。 这在执行也可与用户操作交互的动画时非常有用。
经典的“滑动解锁”手势和播放器视图关闭/展开动画(在音乐应用程序中)是交互式和可中断动画的示例。 您可以用手指开始移动视图,然后松开它,视图将回到原来的位置。 或者,您可以在动画期间捕捉视图并继续用手指拖动它。
以下是我们如何使用UIViewPropertyAnimator
实现动画的简单示例:
let newButtonWidth: CGFloat = 60 let animator = UIViewPropertyAnimator(duration:0.3, curve: .linear) { //1 self.button.frame = CGRect(x: 0, y: 0, width: newButtonWidth, height: newButtonWidth) self.button.center = self.view.center } animator.startAnimation() //2
这是我们正在做的事情:
- 我们通过传递持续时间和动画曲线来调用
UIViewProperty
API。 - 与上述两个 UIView.animate API 不同,动画不会启动,除非您自己指定它,即您完全控制完整的动画过程/流程。
现在,假设您想要对动画进行更多控制。 例如,您想设计和控制动画中的每一帧。 还有另一个 API, animateKeyframes
。 但在深入研究之前,让我们快速了解一下动画中的帧是什么。
什么是frame
?
视图的帧更改/转换的集合,从开始状态到最终状态,被定义为animation
,并且在动画期间视图的每个位置被称为frame
。
动画关键帧
此 API 提供了一种设计动画的方法,您可以定义具有不同时间和过渡的多个动画。 发布此消息后,API 将所有动画简单地集成到一种无缝体验中。
假设我们想以随机方式移动屏幕上的按钮。 让我们看看如何使用关键帧动画 API 来做到这一点。
UIView.animateKeyframes(withDuration: 5, //1 delay: 0, //2 options: .calculationModeLinear, //3 animations: { //4 UIView.addKeyframe( //5 withRelativeStartTime: 0.25, //6 relativeDuration: 0.25) { //7 self.button.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.maxY) //8 } UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.25) { self.button.center = CGPoint(x: self.view.bounds.width, y: start.y) } UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.25) { self.button.center = start } })
这是细分:
-
duration
通过传入动画的持续时间来调用 API。 -
delay
动画的初始延迟持续时间。 -
options
要应用于视图动画的动画曲线类型。 -
animations
采用开发人员/用户设计的所有关键帧动画的块。 -
addKeyFrame
调用 API 来设计每一个动画。 在我们的例子中,我们已经定义了按钮的每一次移动。 我们可以根据需要添加尽可能多的此类动画,并将其添加到块中。 -
relativeStartTime
定义动画块集合中动画的开始时间。 -
relativeDuration
定义此特定动画的总持续时间。 -
center
在我们的例子中,我们只需更改按钮的 center 属性以在屏幕上移动按钮。
这就是最终动画的样子:
核心动画
任何基于 UIKit 的动画都会在内部转换为核心动画。 因此,Core Animation 框架充当任何 UIKit 动画的支持层或骨干。 因此,所有 UIKit 动画 API 都只是以易于使用或方便的方式封装了核心动画 API 的层。
UIKit 动画 API 并没有提供对在视图上执行的动画的太多控制,因为它们主要用于视图的动画属性。 因此,在这种情况下,如果您打算控制动画的每一帧,最好直接使用底层的核心动画 API。 或者,UIView 动画和核心动画也可以结合使用。
UIView + 核心动画
让我们看看如何使用 UIView 和 Core Animation API 重新创建相同的按钮更改动画以及指定时序曲线。
我们可以使用CATransaction
的计时函数,它可以让您指定和控制动画曲线。
让我们看一个使用CATransaction
的计时功能和 UIView 动画组合的按钮大小更改动画的示例:
let oldValue = button.frame.width/2 let newButtonWidth: CGFloat = 60 /* Do Animations */ CATransaction.begin() //1 CATransaction.setAnimationDuration(2.0) //2 CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)) //3 // View animations //4 UIView.animate(withDuration: 1.0) { self.button.frame = CGRect(x: 0, y: 0, width: newButtonWidth, height: newButtonWidth) self.button.center = self.view.center } // Layer animations let cornerAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.cornerRadius)) //5 cornerAnimation.fromValue = oldValue //6 cornerAnimation.toValue = newButtonWidth/2 //7 button.layer.cornerRadius = newButtonWidth/2 //8 button.layer.add(cornerAnimation, forKey: #keyPath(CALayer.cornerRadius)) //9 CATransaction.commit() //10
这是细分:
-
begin
表示动画代码块的开始。 -
duration
整体动画持续时间。 -
curve
表示需要应用于动画的时序曲线。 -
UIView.animate
我们的第一个动画改变了按钮的框架。 -
CABasicAnimation
我们通过将按钮的角cornerRadius
作为键路径来创建CABasicAnimation
对象,因为这就是我们想要动画的对象。 同样,如果您想对关键帧动画进行精细级别控制,则可以使用CAKeyframeAnimation
类。 -
fromValue
表示动画的起始值,即动画必须从其开始的按钮的初始cornerRadius
值。 -
toValue
表示动画的最终值,即动画必须结束的按钮的最终cornerRadius
值。 -
cornerRadius
我们必须将按钮的cornerRadius
属性设置为动画的最终值,否则动画完成后按钮的cornerRadius 值将自动恢复为其初始值。 -
addAnimation
我们通过表示需要执行动画的 Keypath 将包含整个动画过程配置的动画对象附加到图层。 -
commit
表示动画代码块的结束并开始动画。
这是最终动画的样子:
这篇博客非常适合帮助创建更高级的动画,因为它巧妙地引导您完成大多数 Core Animation 框架 API,并提供指导您完成每一步的说明。
UIKitDynamics
UIKit Dynamics 是 UIKit 的物理引擎,它使您能够将任何物理行为(如碰撞、重力、推动、捕捉等)添加到 UIKit 控件。
UIKitDynamicAnimator
这是 UIKit Dynamics 框架的管理类,它管理由任何给定 UI 控件触发的所有动画。
UIKit动态行为
它使您能够将任何物理行为添加到动画师,然后使其能够在附加到它的视图上执行。
UIKitDynamics 的不同类型的行为包括:
-
UIAttachmentBehavior
-
UICollisionBehavior
-
UIFieldBehavior
-
UIGravityBehavior
-
UIPushBehavior
-
UISnapBehavior
UIKitDynamics 的架构看起来像这样。 请注意,项目 1 到 5 可以替换为单个视图。
让我们对按钮应用一些物理行为。 我们将看到如何将重力应用到按钮上,以便它给我们一种处理真实对象的感觉。
var dynamicAnimator : UIDynamicAnimator! var gravityBehavior : UIGravityBehavior! dynamicAnimator = UIDynamicAnimator(referenceView: self.view) //1 gravityBehavior = UIGravityBehavior(items: [button]) //2 dynamicAnimator.addBehavior(gravityBehavior) //3
这是细分:
-
UIKitDynamicAnimator
我们创建了一个UIKitDynamicAnimator
对象,它充当执行动画的协调器。 我们还通过了按钮的 superview 作为参考视图。 -
UIGravityBehavior
我们创建了一个UIGravityBehavior
对象,并将我们的按钮传递到注入此行为的数组元素中。 -
addBehavior
我们已将重力对象添加到动画师。
这应该创建一个动画,如下所示:
我们应该告诉动画师将屏幕底部视为地面。 这就是UICollisionBehavior
作用的地方。var dynamicAnimator : UIDynamicAnimator! var gravityBehavior : UIGravityBehavior! var collisionBehavior : UICollisionBehavior! dynamicAnimator = UIDynamicAnimator(referenceView: self.view) //1 gravityBehavior = UIGravityBehavior(items: [button]) //2 dynamicAnimator.addBehavior(gravityBehavior) //3 collisionBehavior = UICollisionBehavior(items: [button]) //4 collisionBehavior.translatesReferenceBoundsIntoBoundary = true //5 dynamicAnimator.addBehavior(collisionBehavior) //6
-
UICollisionBehavior
我们创建了一个UICollisionBehavior
对象并传递按钮,以便将行为添加到元素中。 -
translatesReferenceBoundsIntoBoundary
启用此属性会告诉动画师将参考视图边界作为结束,在我们的例子中是屏幕的底部。 -
addBehavior
我们在这里为动画师添加了碰撞行为。
现在,我们的按钮应该落地并保持静止,如下所示:
这很整洁,不是吗?
现在,让我们尝试添加一个弹跳效果,让我们的对象感觉更真实。 为此,我们将使用UIDynamicItemBehavior
类。var dynamicAnimator : UIDynamicAnimator! var gravityBehavior : UIGravityBehavior! var collisionBehavior : UICollisionBehavior! var bouncingBehavior : UIDynamicItemBehavior! dynamicAnimator = UIDynamicAnimator(referenceView: self.view) //1 gravityBehavior = UIGravityBehavior(items: [button]) //2 dynamicAnimator.addBehavior(gravityBehavior) //3 collisionBehavior = UICollisionBehavior(items: [button]) //4 collisionBehavior.translatesReferenceBoundsIntoBoundary = true //5 dynamicAnimator.addBehavior(collisionBehavior) //6 //Adding the bounce effect bouncingBehavior = UIDynamicItemBehavior(items: [button]) //7 bouncingBehavior.elasticity = 0.75 //8 dynamicAnimator.addBehavior(bouncingBehavior) //9
-
UIDynamicItemBehavior
我们创建了一个UIDynamicItemBehavior
对象并传递按钮,以便将行为添加到元素中。 -
elasticity
值必须在 0-1 之间,它代表弹性,即物体在被击中时必须在地面上反弹和离开地面的次数。 这就是魔法发生的地方——通过调整这个属性,你可以区分不同种类的物体,比如球、瓶子、硬物等等。 -
addBehavior
我们在这里为动画师添加了碰撞行为。
现在,我们的按钮在落地时应该会弹跳,如下所示:
这个 repo 很有帮助,展示了 UIKitDynamics 的所有行为。 它还提供了用于处理每种行为的源代码。 在我看来,这应该作为在视图上执行 iOS 动画的方法的广泛列表!
在下一节中,我们将简要介绍有助于我们测量动画性能的工具。 我还建议您考虑优化 Xcode 构建的方法,因为它可以节省大量的开发时间。
性能调优
在本节中,我们将研究测量和调整 iOS 动画性能的方法。 作为一名 iOS 开发人员,您可能已经使用过诸如 Memory Leaks 和 Allocations 之类的 Xcode Instruments 来衡量整个应用程序的性能。 同样,也有一些工具可以用来衡量动画的表现。
Core Animation
仪器
尝试使用Core Animation
工具,您应该能够看到您的应用屏幕提供的 FPS。 这是衡量 iOS 应用程序中渲染的任何动画的性能/速度的好方法。
绘画
在显示大量内容(如带有阴影效果的图像)的应用程序中,FPS 大大降低。 在这种情况下,不要将 Image 直接分配给UIImageView
的 image 属性,而是尝试使用 Core Graphics API 在上下文中单独绘制图像。 当在单独的线程而不是主线程中完成时,通过异步执行图像解压缩逻辑,这过度减少了图像显示时间。
光栅化
光栅化是一个用于缓存复杂图层信息的过程,以便这些视图在渲染时不会重绘。 重绘视图是降低 FPS 的主要原因,因此,最好对将要重复使用多次的视图应用光栅化。
包起来
最后,我还总结了 iOS 动画的有用资源列表。 在处理 iOS 动画时,您可能会发现这非常方便。 此外,您还可能会发现这套设计工具在深入研究动画之前作为(设计)步骤很有帮助。
我希望我能够涵盖尽可能多的围绕 iOS 动画的主题。 如果本文中我可能遗漏了什么,请在下面的评论部分告诉我,我很乐意补充!