iOS动画开发:UIViewPropertyAnimator的进阶与交互应用
1. 应用运行与图标重载动画
运行应用程序,点击“Show More”,即可欣赏到流畅的弹簧驱动动画。为了实现图标重载,可以在方法底部添加以下代码:
widgetView.expanded = showsMore
widgetView.reload()
这段代码会触发集合视图的
reloadData()
方法,从而重新加载所有图标。再次尝试动画,你会根据小部件的高度看到不同数量的图标。
2. 内置视图过渡
为了完善这个动画,可以使用
UIViewPropertyAnimator
结合内置的视图过渡效果。这里以将小部件按钮的标题从“Show More”切换为“Show Less”为例,使用交叉淡入淡出效果。在
toggleShowMore(_:)
方法中,定义动画块后、定义动画器之前添加以下代码:
let textTransition = {
UIView.transition(
with: sender,
duration: 0.25,
options: .transitionCrossDissolve,
animations: {
sender.setTitle(
self.showsMore ? "Show Less" : "Show More",
for: .normal)
},
completion: nil)
}
然后在添加动画到动画器的位置,添加
textTransition
并设置0.5的延迟因子:
toggleHeightAnimator?.addAnimations(animations)
toggleHeightAnimator?.addAnimations(textTransition, delayFactor: 0.5)
toggleHeightAnimator?.startAnimation()
这样就可以实现按钮标题的平滑切换效果。如果想尝试其他过渡效果,可以将
.transitionCrossDissolve
替换为
.transitionFlipFromTop
。
3. UIViewPropertyAnimator的关键点
-
模糊效果动画
:使用
UIViewPropertyAnimator可以对UIBlurEffect的值进行动画处理,创建令人印象深刻的模糊动画。 - 自定义动画时序 :通过使用两个控制点的自定义贝塞尔曲线,可以创建自己的自定义动画时序,摆脱预定义的缓动常量。
-
混合过渡API
:可以将预定义的UIKit过渡API(如
UIView.transition(with:duration:options:animations:completion))与其他UIViewPropertyAnimator动画混合使用,它们可以很好地协同工作。
4. 挑战:加法动画
当使用
UIView.animate(withDuration:...)
时,对同一视图属性添加动画是累加的。例如,在视图从点A移动到点B的过程中,如果中途改变主意,决定将视图移动到点C,UIKit会尝试“平滑”地将视图过渡到新的轨迹。
对于这个挑战,如果在
toggleShowMore(_:)
中,前一个动画尚未完成时再次切换菜单状态,应该向现有的
toggleHeightAnimator
添加动画,而不是创建新的动画器。具体步骤如下:
1. 使用
isRunning
检查
toggleHeightAnimator
是否正在运行。
2. 由于
isRunning
可能不太可靠,还需要检查
fractionComplete
是否小于1。
3. 如果动画器已经创建,暂停它,添加新的动画块,然后继续动画。
4. 如果效果不理想,可以尝试将初始弹簧速度调整为零或完全替换弹簧。
以下是示例代码:
if let toggleHeightAnimator = toggleHeightAnimator, toggleHeightAnimator.isRunning, toggleHeightAnimator.fractionComplete < 1 {
toggleHeightAnimator.pauseAnimation()
toggleHeightAnimator.addAnimations(newAnimationBlock)
toggleHeightAnimator.startAnimation()
}
5. 动画状态机
UIViewPropertyAnimator
具有状态机的行为,可以提供有关动画当前状态的各种信息。主要有三个属性可以帮助确定当前状态:
| 属性 | 描述 |
| ---- | ---- |
|
isRunning
| 只读属性,指示动画器的动画是否正在运行。默认值为
false
,调用
startAnimation()
时变为
true
,动画暂停、停止或自然完成时变为
false
。 |
|
isReversed
| 默认值为
false
,表示动画从起始状态播放到结束状态。将其设置为
true
,动画将反向播放到初始状态。 |
|
state
| 只读属性,确定动画器是处于活动状态并正在进行动画,还是处于其他被动状态。默认值为
inactive
,表示动画器刚刚创建且尚未调用任何方法。 |
状态转换规则如下:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([Inactive]):::startend -->|startAnimation()| B(Active):::process
A -->|pauseAnimation()| B
A -->|set fractionComplete| B
B -->|Animation Complete| A
B -->|stopAnimation()| C(Stopped):::process
C -->|finishAnimation(at:)| A
6. 交互式3D触摸动画
在这部分,将创建一个类似于iPhone主屏幕上3D触摸交互的交互式动画。需要一个支持3D触摸的iOS设备或模拟器的Force Touch触控板。
6.1 代码准备
-
打开
WidgetView.swift,找到符合UIPreviewInteractionDelegate的扩展,这些是用户按压小部件视图时UIKit调用的委托方法。 -
这些委托方法已经连接到
LockScreenViewController中的相关方法:-
3D触摸开始时,调用
LockScreenViewController.startPreview(for:)。 -
用户按压更用力(或更轻)时,重复调用
LockScreenViewController.updatePreview(percent:)。 -
预览交互成功完成时,调用
LockScreenViewController.finishPreview()。 -
用户在未完成预览手势的情况下抬起手指时,调用
LockScreenViewController.cancelPreview()。
-
3D触摸开始时,调用
6.2 添加属性
打开
LockScreenViewController.swift
,添加以下属性:
var startFrame: CGRect?
var previewView: UIView?
var previewAnimator: UIViewPropertyAnimator?
let previewEffectView = IconEffectView(blur: .extraLight)
6.3 实现
startPreview(for:)
方法
func startPreview(for forView: UIView) {
previewView?.removeFromSuperview()
if let preview = forView.snapshotView(afterScreenUpdates: false) {
previewView = preview
view.insertSubview(preview, aboveSubview: blurView)
preview.frame = forView.convert(forView.bounds, to: view)
startFrame = preview.frame
addEffectView(below: preview)
}
}
6.4 实现
addEffectView(below:)
方法
func addEffectView(below forView: UIView) {
previewEffectView.removeFromSuperview()
previewEffectView.frame = forView.frame
forView.superview?.insertSubview(
previewEffectView,
belowSubview: forView)
}
6.5 创建动画工厂方法
打开
AnimatorFactory.swift
,添加
grow
方法:
static func grow(
view: UIVisualEffectView,
blurView: UIVisualEffectView
) -> UIViewPropertyAnimator {
view.contentView.alpha = 0
view.transform = .identity
let animator = UIViewPropertyAnimator(
duration: 0.5, curve: .easeIn)
animator.addAnimations {
blurView.effect = UIBlurEffect(style: .dark)
view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}
animator.addCompletion { _ in
blurView.effect = UIBlurEffect(style: .dark)
}
return animator
}
6.6 实现
updatePreview(percent:)
方法
func updatePreview(percent: CGFloat) {
previewAnimator?.fractionComplete = max(0.01, min(0.99, percent))
}
6.7 实现取消和完成方法
- 取消预览 :
func cancelPreview() {
if let previewAnimator = previewAnimator {
previewAnimator.isReversed = true
previewAnimator.startAnimation()
previewAnimator.addCompletion { position in
switch position {
case .start:
self.previewView?.removeFromSuperview()
self.previewEffectView.removeFromSuperview()
default: break
}
}
}
}
- 完成预览 :
func finishPreview() {
previewAnimator?.stopAnimation(false)
previewAnimator?.finishAnimation(at: .end)
previewAnimator = nil
AnimatorFactory.complete(view: previewEffectView).startAnimation()
}
通过以上步骤,就可以实现一个完整的交互式3D触摸动画,当用户按压图标时,会出现模糊背景和菜单展开的效果。
7. 交互式3D触摸动画的细节优化
7.1 动画工厂方法的调整
在之前的
grow
方法中,其完成块在动画反向播放时会出现问题,导致黑暗模糊效果异常显示。因此需要对完成块进行调整,以确保动画在正向和反向播放时都能正确处理模糊效果。
static func grow(
view: UIVisualEffectView,
blurView: UIVisualEffectView
) -> UIViewPropertyAnimator {
view.contentView.alpha = 0
view.transform = .identity
let animator = UIViewPropertyAnimator(
duration: 0.5, curve: .easeIn)
animator.addAnimations {
blurView.effect = UIBlurEffect(style: .dark)
view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}
animator.addCompletion { position in
switch position {
case .start:
blurView.effect = nil
case .end:
blurView.effect = UIBlurEffect(style: .dark)
default:
break
}
}
return animator
}
7.2 解决图标不可交互问题
当用户取消按压图标时,图标快照会覆盖原图标,导致图标无法再次交互。为了解决这个问题,在
cancelPreview
方法中添加代码,在重置动画完成后移除图标快照和图标框架。
func cancelPreview() {
if let previewAnimator = previewAnimator {
previewAnimator.isReversed = true
previewAnimator.startAnimation()
previewAnimator.addCompletion { position in
switch position {
case .start:
self.previewView?.removeFromSuperview()
self.previewEffectView.removeFromSuperview()
default: break
}
}
}
}
7.3 显示图标菜单的动画
为了显示图标菜单,在
AnimatorFactory
中添加
complete
方法,创建一个简单的弹簧动画,用于淡入菜单、重置变换并将视图框架动画到图标上方的位置。
static func complete(view: UIVisualEffectView) ->
UIViewPropertyAnimator {
return UIViewPropertyAnimator(
duration: 0.3,
dampingRatio: 0.7) {
view.contentView.alpha = 1
view.transform = .identity
view.frame = CGRect(
x: view.frame.minX - view.frame.minX / 2.5,
y: view.frame.maxY - 140,
width: view.frame.width + 120,
height: 60
)
}
}
8. 总结与回顾
8.1 核心知识点总结
- UIViewPropertyAnimator的强大功能 :可以实现模糊效果动画、自定义动画时序,还能与预定义的UIKit过渡API混合使用。
-
动画状态管理
:通过
isRunning、isReversed和state三个属性,能够清晰地了解动画的当前状态,并根据状态进行相应的操作。 -
交互式动画实现
:以3D触摸动画为例,展示了如何利用
UIViewPropertyAnimator创建交互式动画,包括动画的启动、暂停、反向播放以及完成处理。
8.2 操作流程回顾
| 操作步骤 | 描述 |
|---|---|
| 运行应用与图标重载 |
在方法底部添加代码触发集合视图的
reloadData()
方法,重新加载图标。
|
| 内置视图过渡 |
在
toggleShowMore(_:)
方法中添加过渡动画,实现按钮标题的平滑切换。
|
| 加法动画挑战 |
检查
toggleHeightAnimator
状态,添加新动画块实现加法动画。
|
| 动画状态机 |
根据
isRunning
、
isReversed
和
state
属性管理动画状态。
|
| 交互式3D触摸动画 | 从代码准备、属性添加、方法实现到动画工厂方法创建,逐步实现完整的交互式动画。 |
8.3 未来展望
通过对
UIViewPropertyAnimator
的深入学习和实践,我们可以创建出更加复杂和精彩的动画效果。在未来的开发中,可以进一步探索其与其他动画技术的结合,为用户带来更加流畅和富有交互性的界面体验。同时,不断尝试新的动画创意和应用场景,提升应用的吸引力和竞争力。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(运行应用与图标重载):::process
B --> C(内置视图过渡):::process
C --> D(加法动画挑战):::process
D --> E(动画状态机管理):::process
E --> F(交互式3D触摸动画实现):::process
F --> G([结束]):::startend
综上所述,掌握
UIViewPropertyAnimator
的使用方法和技巧,对于iOS动画开发至关重要。通过不断地实践和优化,我们可以创造出更加出色的动画效果,为用户带来更好的视觉体验。
超级会员免费看
7

被折叠的 条评论
为什么被折叠?



