博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
系统学习iOS动画之五:使用UIViewPropertyAnimator
阅读量:5998 次
发布时间:2019-06-20

本文共 30934 字,大约阅读时间需要 103 分钟。

本文是我学习 笔记中的一篇。

文中详细代码都放在我的Github上 。

UIViewPropertyAnimator是从iOS10开始引入,它能够创建易于交互,可中断和/或可逆的视图动画。

这个类让某些类型的视图动画更容易创建,值得学习。

UIViewPropertyAnimator可以在同一个类中方便地将许多API包装在一起,这样更容易使用。

此外,这个新类不能完全取代了UIView.animate(withDuration...)API集。

内容预览:

本文的四个章节都是使用同一个项目 LockSearch

20-UIViewPropertyAnimator入门

在iOS10之前,创建基于视图的动画的唯一选择是UIView.animate(withDuration: ...)I,但这组API没有为开发人员提供暂停或停止已经运行的动画的处理方式。此外,对于反转,加速或减慢动画,开发人员只能使用基于图层的CAAnimation(核心动画)。

UIViewPropertyAnimator就是为了解决上述问题而出现的,它是一个允许保持运行动画的类,允许开发者调整当前运行的动画,并提供有关动画当前状态的详细信息。

当然,简单单一的视图动画直接使用UIView.animate(withDuration: ...)就可以了。

基础动画

本章的 LockSearch 。 类似于iOS锁屏时的屏幕。 初始视图控制器有搜索栏,单个窗口小部件和编辑按钮等:

已经实现了一些与动画无关的功能。 例如,如果点击Show More按钮,窗口小部件将展开并显示更多项目。 如果点击编辑,会转到另一个视图控制器,这是一个简单的TableView。

当然,该项目只是模拟iOS中的锁定屏幕,用来学习动画,没有实际的功能,。

打开LockScreenViewController.swift并向该视图控制器添加一个新的viewWillAppear(_:)方法:

override func viewWillAppear(_ animated: Bool) {    tableView.transform = CGAffineTransform(scaleX: 0.67, y: 0.67)    tableView.alpha = 0}复制代码

为了创建简单的缩放和淡入淡出视图动画,首先缩小整个表视图并使其透明。

接下来,在视图控制器的视图出现在屏幕上时创建一个动画师。 将以下内容添加到LockScreenViewController

override func viewDidAppear(_ animated: Bool) {    let scale = UIViewPropertyAnimator.init(duration: 0.33, curve: .easeIn) {    }}复制代码

在这里,您使用UIViewPropertyAnimator的一个:UIViewPropertyAnimator.init(duration:curve:animations:)

通过构造器创建动画实例并设置动画的总持续时间和时间曲线。 后一个参数的类型为UIViewAnimationCurve,这是一个枚举类型,有四个类型:easeInOuteaseIneaseOutlinear。这与UIView.animate(withDuration:...)中的是类似的。

添加动画

viewDidAppear(_:)中添加:

scale.addAnimations {    self.tableView.alpha = 1.0}复制代码

使用addAnimations添加动画代码块,就像UIView.animate(withDuration...)的闭包参数animations。 使用动画师的不同之处在于可以添加多个动画块。

除了能够有条件地构建复杂的动画外,还可以添加具有不同延迟的动画。 另一个版本的addAnimations,有两个参数: animation 动画代码 delayFactor 动画开始前的延迟

delayFactorUIView.animate(withDuration...)delay不同,它介于0.0到1.0,不是绝对时间是相对时间。

在同一个动画师添加第二个动画,但有一些延迟。继续在上面的代码后添加:

scale.addAnimations({    self.tableView.transform = .identity}, delayFactor: 0.33)复制代码

实际延迟时间是delayFactor乘以动画师的剩余持续时间(remaining duration)。 目前尚未启动动画,因此剩余持续时间等于总持续时间。 所以在上面的情况:

delayFactor(0.33) * remainingDuration(=duration 0.33) = delay of 0.11 seconds复制代码

为什么第二个参数不是一个简单的秒数值? 想象动画师已经在运行了,你决定在中途添加一些新的动画。 在这种情况下,剩余持续时间不会等于总持续时间,因为自启动动画以来已经过了一段时间。

在这种情况下,delayFactor将允许开发者根据剩余可用时间设置延迟动画。 此外,这样设计也确保了不能将延迟设置为长于剩余运行时间。

添加完成闭包

viewDidAppear(_:)中添加:

scale.addCompletion { (_) in    print("ready")}复制代码

addCompletion(_:)就是动画完成闭包,当然,它也可多次调用,来完成多了处理程序。

下面要启动动画,在viewWillAppear(_:)的末尾添加:

scale.startAnimation()复制代码

提取动画

为了代码的清晰,可以把动画代码集中放到一个类中。

创建一个名为AnimatorFactory.swift的新文件,并将其默认内容替换为:

import UIKitclass AnimatorFactory {  }复制代码

然后添加一个类型方法,其中包含刚刚编写的动画代码,但默认情况下不运行动画,而是返回动画师:

static func scaleUp(view: UIView) -> UIViewPropertyAnimator {    let scale = UIViewPropertyAnimator(duration: 0.33, curve: .easeIn)    scale.addAnimations {        view.alpha = 1.0    }    scale.addAnimations({        view.transform = .identity    }, delayFactor: 0.33)    scale.addCompletion { (_) in                         print("ready")                        }    return scale}复制代码

该方法将视图作为参数,并在该视图上创建所有动画,最后它返回准备好的动画师。

LockScreenViewController中的viewDidAppear(_:)替换为:

override func viewDidAppear(_ animated: Bool) {    AnimatorFactory.scaleUp(view: tableView).startAnimation()}复制代码

这样看上去代码更加简洁,清晰,把动画代码从视图控制器移出。

这个动画师工厂?类AnimatorFactory集中处理动画代码,这是设计模式中的工厂模式的一个简单应用。?

运行动画师

当用户使用搜索栏时,将淡入模糊图层(blurView),并在用户完成搜索时将其淡出。

LockScreenViewController类添加一个新方法:

func toggleBlur(_ blurred: Bool) {    UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0.1, options: .curveEaseOut, animations: {        self.blurView.alpha = blurred ? 1 : 0    }, completion: nil)}复制代码

UIViewPropertyAnimator.runningPropertyAnimator(withDuration:...)UIView.animate(withDuration:...)有完全相同的参数,使用也相同。

虽然看起来这可能是一种**“即发即忘”**(“fire-and-forget” )的API,但请注意它确实会返回一个动画实例。 因此,您可以添加更多动画,更多完成块,并且通常与当前正在运行的动画进行交互。

现在让我们看看淡入淡出动画的样子。 LockScreenViewController已设置为搜索栏的委托,因此您只需实现所需的方法即可在正确的时间触发动画。

以扩展的方式为LockScreenViewController遵守搜索栏的代理协议:

extension LockScreenViewController: UISearchBarDelegate {    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {    toggleBlur(true)  }    func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {    toggleBlur(false)  }}复制代码

要为用户提供取消搜索的功能,还要添加以下两种方法:

func searchBarResultsListButtonClicked(_ searchBar: UISearchBar) {    searchBar.resignFirstResponder()  }    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {    if searchText.isEmpty{      searchBar.resignFirstResponder()    }  }复制代码

这将允许用户通过点击右侧按钮解除搜索。

运行,效果:

点按搜索栏文本字段,小部件在模糊效果视图下消失;点击搜索栏右侧的按钮时,模糊视图会淡出。

基础关键帧动画

UIViewPropertyAnimator也可以使用UIView.addKeyframe()。下面创建一个简单的图标抖动动画来展示。

AnimatorFactory中添加类型方法:

static func jiggle(view: UIView) -> UIViewPropertyAnimator {    return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.33, delay: 0      , animations: {        UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations: {          view.transform = CGAffineTransform(rotationAngle: -.pi/8)        })        UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75, animations: {          view.transform = CGAffineTransform(rotationAngle: +.pi/8)        })        UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 1.0, animations: {          view.transform = CGAffineTransform.identity        })    }, completion: { (_) in          })  }复制代码

第一个关键帧向左旋转,第二个关键帧向右旋转,最后第三个关键帧回到原点 。

要确保图标保持在其初始位置,在完成闭包中添加:

view.transform = .identity复制代码

下面就可以在想要运行这个动画的视图上添加动画了。

打开IconCell.swift(该文件位于Widget子文件夹中)。这是自定义单元类,对应于窗口小部件视图中的每个图标。 在IconCell中添加:

func iconJiggle() {    AnimatorFactory.jiggle(view: icon)}复制代码

现在Xcode抱怨AnimatorFactory.jiggle方法返回一个结果没有被使用,这是Xcode善意的提醒?。

这个问题很容易解决,只需要在jiggle方法前添加@discardableResult,让Xcode知道这个方法的结果我不要了?。

discardableResult的:

Apply this attribute to a function or method declaration to suppress the compiler warning when the function or method that returns a value is called without using its result.

@discardableResult  static func jiggle(view: UIView) -> UIViewPropertyAnimator {复制代码

要最终运行动画,在WidgetView.swiftcollectionView(_:didSelectItemAt:)中添加:

if let cell = collectionView.cellForItem(at: indexPath) as? IconCell {    cell.iconJiggle()}复制代码

效果:

提取模糊动画

把前面的模糊动画也提取到AnimatorFactory中。

@discardableResultstatic func fade(view: UIView, visible: Bool) -> UIViewPropertyAnimator {    return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0.1, options: .curveEaseOut, animations: {        view.alpha = visible ? 1.0 : 0.0    }, completion: nil)}复制代码

替代LockScreenViewController中的toggleBlur(_:)方法:

func toggleBlur(_ blurred: Bool) {    AnimatorFactory.fade(view: blurView, visible: blurred)}复制代码

防止动画重叠

如何检查动画师当前是否正在执行其动画?

如果在同一个图标上快速连续点击,会发现抖动动画没有结束就重新开始了。

解决这个问题,就需要检测视图是否有动画正在运行。

IconCell添加一个属性,并修改iconJiggle()

var animator: UIViewPropertyAnimator?  func iconJiggle() {    if let animator = animator, animator.isRunning {      return    }    animator = AnimatorFactory.jiggle(view: icon)  }复制代码

对比可以发现有所不同:

21-深入UIViewPropertyAnimator

上一章节学习了UIViewPropertyAnimator的基本使用,这一章节学习更多关于UIViewPropertyAnimator的知识。

本章的 使用上一章节完成的项目。

自定义动画计时

前文已经多次提到:easeInOuteaseIneaseOutlinear(可以理解为物体运动轨迹的曲线类型)。可以参考 或者,这边就不再介绍了。

内置时间曲线

目前,当您激活搜索栏时,您会在窗口小部件顶部的模糊视图中淡入淡出。 在此示例中,您将删除该淡入淡出动画并为模糊效果本身设置动画。

之前,激活搜索栏时,就会有一个模糊视图中淡入淡出效果。这个部分删除这个效果,修改成对模糊效果本身设置动画。什么意思呢? 看完下面的操作,应该能明白。

LockScreenViewController类添加一个新方法:

func blurAnimations(_ blured: Bool) -> () -> Void {    return {         self.blurView.effect = blured ? UIBlurEffect(style: .dark) : nil    self.tableView.transform = blured ? CGAffineTransform(scaleX: 0.75, y: 0.75) : .identity    self.tableView.alpha = blured ? 0.33 : 1.0    }}复制代码

删除viewDidLoad()中的两行代码:

blurView.effect = UIBlurEffect(style: .dark)    blurView.alpha = 0复制代码

替代toggleBlur(_:)内容为:

func toggleBlur(_ blurred: Bool) {    UIViewPropertyAnimator(duration: 0.55, curve: .easeOut, animations: blurAnimations(blurred)).startAnimation()}复制代码

运行,效果:

请注意模糊不仅仅是淡入或淡出,实际上它会在效果视图中插入模糊量。

贝塞尔曲线

有时想要对动画的时间非常具体时,使用这些曲线简单地“开始减速”或“慢慢结束”是不够的。

在 中学习了使用CAMediaTimingFunction控制图层动画的时间。

之前没有了解背后的原理贝塞尔曲线,这边介绍一下它。这边的内容也可应用到图层动画中。

贝塞尔曲线是什么?

让我们从简单的事情开始 —— 一条线。它非常简洁,需要在屏幕上画一条线,只需要定义它的两个点的坐标,开始 (A) 和结束 (B)

现在让我们来看看曲线。曲线比线条更有趣,因为它们可以在屏幕上绘制任何东西。例如:

在上面看到的是四条曲线放在一起;它们的两端在小白方块的地方相遇。图中有趣的是小绿圈,它们定义了每条曲线。

所以曲线不是随机的。它们也有一些细节,就像线条一样,可以帮助我们通过坐标定义它们。

您可以通过向线条添加控制点来定义曲线。 让我们在之前的行中添加一个控制点:

可以想象由连接到线的铅笔绘制的曲线,其起点沿着线AC移动,其终点沿着线CB移动:

网上找了一个动图:

具有一个控制点的Bézier曲线称为 二次曲线。有两个控制点的Bézier曲线叫做 三次曲线(立方贝塞尔曲线)。 我们使用的内置曲线就是三次曲线。

核心动画使用始终以坐标(0,0)开始的三次曲线,它表示动画持续时间的开始。 当然,这些时间曲线的终点始终是(1,1),表示 动画的持续时间和进度的结束。

让我们来看看 ease-in 曲线:

随着时间的推移(在坐标空间中从左向右水平移动),曲线在垂直轴上的进展非常小,然后大约在动画持续时间的一半时间后,曲线在垂直轴上的进展非常大,最终在(1, 1)处结束。

ease-outease-in-out曲线分别是:

现在已了解Bézier曲线的工作原理,剩下的问题是如何在视觉上设计一些曲线并获得控制点的坐标,方便可以将它们用于iOS动画。

可以使用网站:。 这是计算机科学研究员和演讲者Lea Verou的非常方便的网站。 它可以拖动立方Bézier的两个控制点并查看即时动画预览,非常nice??。

上面贝塞尔的原理说的不够深刻?‍♀️,现在只需了解曲线,通过两个控制点可以画曲线。

接下来,向项目中添加自定义计时动画。

LockScreenViewController中的toggleBlur()的现有动画替换为:

func toggleBlur(_ blurred: Bool) {    UIViewPropertyAnimator(duration: 0.55, controlPoint1: CGPoint(x: 0.57, y: -0.4), controlPoint2: CGPoint(x: 0.96, y: 0.87), animations: blurAnimations(blurred)).startAnimation()}复制代码

这边的controlPoint1controlPoint2两个点,就是我们自定义三次曲线的控制点。

可以通过 网站来选着控制点。

弹簧动画

另一个便利构造器UIViewPropertyAnimator(duration:dampingRatio:animations:),用于定义弹簧动画。

这与UIView.animate(withDuration: delay: usingSpringWithDamping: initialSpringVelocity: options: animations: completion:)类似,只不过初始速度为0。

自定义时间曲线

UIViewPropertyAnimator类还有一个构造器UIViewPropertyAnimator(duration:timingParameters:)

参数timingParameters必须遵守UITimingCurveProvider协议,有两个类可供我们使用:和。

下面看看这个构造器的使用方式。

阻尼和速度

添加阻尼和速度的方式如下:

let spring = UISpringTimingParameters(dampingRatio:0.5, initialVelocity: CGVector(dx: 1.0, dy: 0.2))let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring)复制代码

注意初始速度initialVelocity矢量类型,这个参数是一个可选参数。

自定义弹簧动画

如果想对弹簧动画更加具体的设置,可以UISpringTimingParameters的另一个构造器init(mass:stiffness:damping:initialVelocity:),代码如下:

let spring = UISpringTimingParameters(mass: 10.0, stiffness: 5.0, damping: 30, initialVelocity: CGVector(dx: 1.0, dy: 0.2))let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring) 复制代码

上面这些参数的工作原理,可以查看之前的文章。

自动布局动画

前面的文章 学习了自动布局动画。

使用UIViewPropertyAnimator的布局约束动画与使用UIView.animate(withDuration: ...)创建它们的方式非常相似。 诀窍是更新约束,在动画块中调用layoutIfNeeded()

AnimatorFactory中添加一个新的工厂方法:

@discardableResultstatic func animateConstraint(view: UIView, constraint: NSLayoutConstraint, by: CGFloat) -> UIViewPropertyAnimator {    let spring = UISpringTimingParameters(dampingRatio: 0.55)    let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring)    animator.addAnimations {        constraint.constant += by        view.layoutIfNeeded()    }    return animator}复制代码

LockScreenViewControllerviewWillAppear里添加:

dateTopConstraint.constant -= 100view.layoutIfNeeded()复制代码

viewDidAppear里添加:

AnimatorFactory.animateConstraint(view: view, constraint: dateTopConstraint, by: 150).startAnimation()复制代码

这让时间标签的位置,在应用打开时有一个动画。

接下来,在添加一个约束动画。当点击“Show more”时,窗口小部件会加载内容,并需要更改其高度约束。

重新定义WidgetCell.swift中的toggleShowMore(_:)方法:

@IBAction func toggleShowMore(_ sender: UIButton) {    self.showsMore = !self.showsMore    let animations = {        self.widgetHeight.constant = self.showsMore ? 230 : 130        if let tableView = self.tableView {            tableView.beginUpdates()            tableView.endUpdates()            tableView.layoutIfNeeded()        }    }    let spring = UISpringTimingParameters(mass: 30, stiffness: 10, damping: 300, initialVelocity: CGVector(dx: 5, dy: 0))    toggleHeightAnimator = UIViewPropertyAnimator(duration: 0.0, timingParameters: spring)    toggleHeightAnimator?.addAnimations(animations)    toggleHeightAnimator?.startAnimation()}复制代码

toggleShowMore(_:)方法的底部,添加以下代码用来加载窗口小部件中的图标:

widgetView.expanded = showsMorewidgetView.reload()复制代码

视图过渡

在,学习了视图过渡。现在用UIViewPropertyAnimator做视图过渡。

显示更多按钮的title,"Show More" 和 "Show Less" 两者相互淡入淡出动画。

toggleShowMore(_ :)toggleHeightAnimator定义之前添加这段代码:

let textTransition = {    UIView.transition(with: sender, duration: 0.25, options: .transitionCrossDissolve, animations: {        sender.setTitle(self.showsMore ? "Show Less" : "Show More", for: .normal)    }, completion: nil)}复制代码

toggleHeightAnimator开始之前添加:

toggleHeightAnimator?.addAnimations(textTransition, delayFactor: 0.5)复制代码

这将改变按钮标题,具有很好的交叉淡入淡出效果:

效果也可以尝试.transitionFlipFromTop

22-用UIViewPropertyAnimator进行交互式动画

前面两个章节介绍了许多UIViewPropertyAnimator 的使用,例如基本动画,自定义计时和弹簧动画,以及动画的提取。但是,与以前视图动画 “即发即忘”("fire-and-forget")API相比,尚未研究使UIViewPropertyAnimator真正有趣的地方。

UIView.animate(withDuration:...)提供了动画的设置方法,但是一旦定义动画结束状态,那么动画就会开始执行,而无法控制。

但是如果我们想在动画运行时与之交互,怎么办? 细说,就是动画不是静态的,而是由用户手势或麦克风输入驱动的,就像在前面图层动画 所学的一样。

使用UIViewPropertyAnimator 创建的动画是完全交互式的:可以启动,暂停,改变速度,甚至可以直接调整进度。

由于UIViewPropertyAnimator可以同时驱动预设动画和交互式动画,因而在描述动画师当前的状态时,就有点复杂了?。下面就看看如何处理动画师的状态。

本章的 使用上一章节完成的项目。

动画状态机

UIViewPropertyAnimator可以检查动画是否已启动(isRunning),是否已暂停或完全停止(state),或动画是否已颠倒(isReversed)。

UIViewPropertyAnimator有三个描述当前状态的属性:

isRunning(只读):动画当前是否处于运动状态。 默认为false,在调用startAnimation()时变为true,如果暂停或停止动画,或者动画自然完成,它将再次变为false

isReversed:默认为false,因为我们总是向前开始动画,即动画从开始状态播放到结束状态。 如果更改为true,则动画将颠倒,即从介绍状态到开始状态。

state (只读):

state默认为inactive,这通常意味着刚刚创建了动画师,并且还没有调用任何方法。请注意,这与将isRunning设置为false不同,isRunning实际上只关注正在进行的动画,而当state处于inactive时,这实际上意味着动画师还没有做任何事情。

state 变成 active的情况有:

  • 调用startAnimation()来启动动画
  • 在没有开始动画的情况下调用pauseAnimation()
  • 设置fractionComplete属性以将动画“倒回”到某个位置

动画自然完成后,state切换回.inactive

如果在动画师上调用stopAnimation(),它会将其state属性设置为.stopped。在这种状态下,你唯一能做的就是完全放弃动画师或者调用finishAnimation(at:)来完成动画并让动画师回到.inactive

正如你可能想到的那样,UIViewPropertyAnimator只能按特定顺序在状态之间切换。 它不能直接从inactivestopped,也不能从stopped直接转为active

如果设置了pausesOnCompletion,一旦动画师完成了动画的运行而不是自动停止,而是暂停。 这将使我们有机会在暂停状态下继续使用它。

状态流程图:

可能有点绕,之后的使用中,如果有疑问,可以再回到这个部分查看。

交互式3D touch动画

从这个部分开始,将学习创建类似于3D touch交互的交互式动画:

注意:对于本章项目,需要兼容3D touch的iOS设备(没记错的话是6S+)。

听闻?,3D touch这个技术会被在iPhone上取消,好吧,这边是学习类似3D touch 的动画,它的未来如何,就不过问了。

3D touch的动画,可以这样描述:当我们手指按压屏幕上的图标时,动画交互式开始,背景越来越模糊,从图标旁渐渐呈现一个菜单,这个过程会随着手指按压的力度变化而前后变化。

放慢的效果为:

WidgetView.swift中,WidgetView通过扩展遵守UIPreviewInteractionDelegate协议。这个协议中就包括了3D touch过程中一些委托方法。

为了让您开始开发动画本身,UIPreviewInteractionDelegate方法已经连接到LockScreenViewController上调用相关方法。 WidgetView中的代码如下:

  • 3D Touch开始时调用LockScreenViewController.startPreview(for:)
  • 当用户按下的过程中,可能更硬(或更柔和)时,反复调用LockScreenViewController.updatePreview(percent:)
  • 当peek交互成功完成时,调用LockScreenViewController.finishPreview()
  • 最后,如果用户在未完成预览手势的情况下抬起手指,则调用LockScreenViewController.cancelPreview()

LockScreenViewController中添加这三个属性,您需要这些属性来创建窥视交互:

var startFrame: CGRect?var previewView: UIView?var previewAnimator: UIViewPropertyAnimator?复制代码

startFrame 来跟踪动画的开始位置。

previewView 图标的快照视图,动画期间暂时使用它。 previewAnimator 将成为驱动预览动画的交互式动画师。

再添加一个属性以保持模糊效果以显示图标框:

let previewEffectView = IconEffectView(blur: .extraLight)复制代码

IconEffectView是自定义的UIVisualEffectView的子类,它包含单个标签的简单模糊视图,使用它来模拟从按下的图标弹出的菜单:

LockScreenViewController遵守WidgetsOwnerProtocol协议的扩展中,实现startPreview(for:)方法:

func startPreview(for forView: UIView) {    previewView?.removeFromSuperview()    previewView = forView.snapshotView(afterScreenUpdates: false)    view.insertSubview(previewView!, aboveSubview: blurView)}复制代码

WidgetsOwnerProtocol协议是一个自定义协议。

只要用户开始按下图标,WidgetView就会调用startPreview(for:)。 参数for是用户开始手势的集合单元格图像。

首先删除任何现有的previewView视图,以防万一在屏幕上留下之前的视图。 然后,您可以创建集合视图图标的快照,最后将其添加到模糊效果视图上方的屏幕上。

运行,按压图标。发现图标出现在左上角!?

因为尚未设置其位置。 继续添加:

previewView?.frame = forView.convert(forView.bounds, to: view)startFrame = previewView?.frameaddEffectView(below: previewView!)复制代码

现在图标副本位置正确了,完全覆盖在原有图标上。 startFrame用来存储起始frame,以供之后使用。

函数addEffectView(below:)添加图标快照下方的模糊框。代码为:

func addEffectView(below forView: UIView) {    previewEffectView.removeFromSuperview()    previewEffectView.frame = forView.frame    forView.superview?.insertSubview(previewEffectView, belowSubview: forView)}复制代码

下面创建动画本身,在AnimatorFactory中添加类方法:

static func grow(view: UIVisualEffectView, blurView: UIVisualEffectView) -> UIViewPropertyAnimator {    view.contentView.alpha = 0    view.transform = .identity    let animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeIn)    return animator}复制代码

两个参数,view是动画视图,blurView 是动画的模糊背景。

在返回动画师之前,为动画师添加动画和完成闭包:

animator.addAnimations {    blurView.effect = UIBlurEffect(style: .dark)    view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)}animator.addCompletion { (_) in    blurView.effect = UIBlurEffect(style: .dark)}复制代码

动画代码为blurView创建了模糊过渡,为view创建一个普通的转换。

之后,在LockScreenViewController.swiftstartPreview()中完成调用:

previewAnimator = AnimatorFactory.grow(view: previewEffectView, blurView: blurView)复制代码

现在运行,还没有效果,还需要实现updatePreview(percent:)方法:

func updatePreview(percent: CGFloat) {    previewAnimator?.fractionComplete = max(0.01, min(0.99, percent))}复制代码

WidgetView被按压时,上面个方法会被重复调用。fractionComplete在0.01和0.99范围内,因为我不希望在动画才这段结束,我另外指定的方法完成或取消动画。

运行,效果(放慢):

你会(惊喜!)需要更多的动画师。 打开AnimatorFactory.swift并添加一个动画师,它可以解除你的“成长”动画师所做的一切。 您需要此动画师的一种情况是用户取消手势。 当您需要清理UI时,另一个是成功交互的最后阶段。

AnimatorFactory中添加方法:

static func reset(frame: CGRect, view: UIVisualEffectView, blurView: UIVisualEffectView) -> UIViewPropertyAnimator {    return UIViewPropertyAnimator(duration: 0.5, dampingRatio: 0.7, animations: {        view.transform = .identity        view.frame = frame        view.contentView.alpha = 0        blurView.effect = nil    })}复制代码

此方法的三个参数分别是原始动画的起始帧,动画视图和背景模糊视图。 动画块将重置交互开始之前状态中的所有属性。

LockScreenViewController.swift中,实现WidgetsOwnerProtocol协议的另一个方法:

func cancelPreview() {    if let previewAnimator = previewAnimator {        previewAnimator.isReversed = true        previewAnimator.startAnimation()    }}复制代码

cancelPreview()WidgetView被按压后,突然抬起手指时调用的方法,取消正在进行的手势。

到目前为止,你还没有开始你的动画师。 您一直在重复设置fractionComplete,这会以交互方式驱动动画。 但是,一旦用户取消交互,您就无法继续以交互方式驱动动画,因为您没有更多输入。 相反,通过将isReversed设置为true并调用startAnimation(),可以将动画播放到其初始状态。 现在这是UIView.animate(withDuration: ...)无法做到的事情!

再试一次互动。按下动画的一半,然后开始测试cancelPreview()

当您抬起手指时动画会正确播放,但最终黑暗模糊会突然重新出现。

这个问题植根于你的成长动画师的代码。切换回AnimatorFactory.swift并查看grow中的代码(view:UIVisualEffectView,blurView:UIVisualEffectView) - 更具体地说,这部分:

animator.addCompletion { (_) in  blurView.effect = UIBlurEffect(style: .dark)}复制代码

动画可以向前或向后播放,需要在完成闭包中处理。

addCompletion() 的闭包的参数用_省略掉了,它其实是一个枚举类型UIViewAnimatingPosition,表示动画当前进行的情况。它的值可有三个,可以是.start.end.current

将完成闭包替代为:

animator.addCompletion { (position) in  switch position {      case .start:      blurView.effect = nil      case .end:      blurView.effect = UIBlurEffect(style: .dark)      default:      break  }}复制代码

如果动画被返回,则删除模糊效果。 如果成功完成,则明确将效果调整为暗模糊效果。

现在有一个新问题。 如果取消对某个图标上的按压,则无法再按下它! 这是因为图标快照仍然位于原始图标上方,挡住按压手势操作。 要解决该问题,值需要在重置动画完成后立即删除快照。

LockScreenViewController.swiftcancelPreview()中继续添加:

previewAnimator.addCompletion { (position) in  switch position {  case .start:    self.previewView?.removeFromSuperview()    self.previewEffectView.removeFromSuperview()  default:    break  }}复制代码

注意:addCompletion(_:)可以调用多次,不会被下一个替代。

让我们再添加一个动画师来显示图标菜单。 切换到AnimatorFactory.swift并添加到它:

static func complete(view: UIVisualEffectView) -> UIViewPropertyAnimator {  return UIViewPropertyAnimator(duration: 0.3, dampingRatio: 0.7, animations: {    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)  })}复制代码

这一次你创建了一个简单的弹簧动画师。 对于动画师,您可以执行以下操作:

  • 淡入“自定义操作”菜单项。
  • 重置转换。
  • 将视图框架直接设置为图标正上方的位置。

菜单的位置根据用户按下的图标而变化。

您将水平位置设置为 view.frame.minX - view.frame.minX/2.5,如果图标位于屏幕左侧,则显示右侧菜单,如果图标位于左侧,则显示左侧菜单在屏幕的右侧。请参阅以下差异:

动画师准备好了,所以打开LockScreenViewController.swift并在WidgetsOwnerProtocol扩展中添加最后一个必需的方法:

func finishPreview() {    previewAnimator?.stopAnimation(false)    previewAnimator?.finishAnimation(at: .end)    previewAnimator = nil}复制代码

当您感觉到触觉反馈时,用户按下3D触摸手势时会调用finishPreview()。

stopAnimation(_:)是停止当前在屏幕上运行的动画。参数为false,动画师状态为stopped;参数为true,动画师状态为inactive并清除所有动画,而且不调用完成闭包。

一旦你将动画师置于停止状态,你就有了一些选择。你在finishPreview()中追求的是告诉动画师完成它的最终状态。因此,您调用finishAnimation(at:.end);这将使用计划动画的目标值更新所有视图并调用您的完成。

此手势不再需要previewAnimator,因此您可以将其删除。

您可以使用以下方法之一调用finishAnimation(at :):

start:将动画重置为初始状态。 current:从动画的当前进度更新视图的属性并完成。

调用finishAnimation(at:)后,您的动画师处于inactive

回到Widgets项目。由于你摆脱了预览动画师,你可以运行完整的动画师来显示菜单。将以下内容附加到finishPreview()的末尾:

AnimatorFactory.complete(view: previewEffectView).startAnimation()复制代码

运行,按压图标:

关闭模糊视图

目前,菜单弹出,模糊视图显示后,还没有回到原来视图的操作,下面添加这个操作。

finishPreview()中添加以下代码,以准备交互式模糊:

blurView.effect = UIBlurEffect(style: .dark)blurView.isUserInteractionEnabled = trueblurView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissMenu)))复制代码

先确保将模糊效果设置为.dark,然后模糊视图本身上启用用户交互,并未模糊视图添加点击手势操作,允许用户点击图标周围的任何位置用来关闭菜单。

dismissMenu()代码为:

@objc func dismissMenu() {    let reset = AnimatorFactory.reset(frame: startFrame!, view: previewEffectView, blurView: blurView)    reset.addCompletion { (_) in                         self.previewEffectView.removeFromSuperview()                         self.previewView?.removeFromSuperview()                         self.blurView.isUserInteractionEnabled = false                        }    reset.startAnimation()}复制代码

交互式关键帧动画

在学习了 用UIViewPropertyAnimator制作关键帧动画,现在再给关键帧动画添加交互式操作。

为了尝试一下,你将为成长动画添加一个额外的元素 - 在用户按下图标时以交互方式擦洗的元素。

删除AnimatorFactorygrow()方法中的代码:

animator.addAnimations {  blurView.effect = UIBlurEffect(style: .dark)  view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)}复制代码

替换为:

animator.addAnimations {    UIView.animateKeyframes(withDuration: 0.5, delay: 0.0, animations: {        UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1.0, animations: {            blurView.effect = UIBlurEffect(style: .dark)            view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)        })        UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {            view.transform = view.transform.rotated(by: -.pi/8)        })    })}复制代码

第一个关键帧运行您之前的相同动画。 第二个关键帧是简单旋转,效果:

23-用UIViewPropertyAnimator自定义视图控制器转场

在中,学习了如何创建自定义视图控制器转场。这个章节学习使用UIViewPropertyAnimator来自定义视图控制器转场。

本章的 使用上一章节完成的项目。

静态视图控制器转场

现在,点击**”Edit“**按钮时,体验非常糟糕?。

首先创建一个新文件PresentTransition.swift,从名字也能看出这个类是用来转场的。 将其默认内容替换为:

import UIKitclass PresentTransition: NSObject, UIViewControllerAnimatedTransitioning {    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {        return 0.75    }    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {    }}复制代码

UIViewControllerAnimatedTransitioning协议已经在中学过。

我将创建一个转场动画:原视图逐渐模糊图,新视图慢慢移动出来。

PresentTransition中添加一个新方法:

func transitionAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {    let duration = transitionDuration(using: transitionContext)        let container = transitionContext.containerView    let to = transitionContext.view(forKey: .to)!        container.addSubview(to)}复制代码

在上面的代码中,为视图控制器转场做了一些必要的准备工作。 首先获取动画持续时间,然后获取目标视图控制器的视图,最后将此视图添加到过渡容器中。

接下来,可以设置动画并运行它。 将下面代码添加到上面的方法transitionAnimator(using:)中:

to.transform = CGAffineTransform(scaleX: 1.33, y: 1.33).concatenating(CGAffineTransform(translationX: 0.0, y: 200))to.alpha = 0复制代码

这会向上伸展,然后向下移动目标视图控制器的视图,最后将其淡出。

to.alpha = 0之后添加动画师来运行转换:

let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut)animator.addAnimations({    to.transform = CGAffineTransform(translationX: 0.0, y: 100)}, delayFactor: 0.15)animator.addAnimations({    to.alpha = 1.0}, delayFactor: 0.5)复制代码

动画师中有两个动画:将目标视图控制器的视图移动到最终位置和淡入。

最后添加完成闭包:

animator.addCompletion { (_) in                             transitionContext.completeTransition(!transitionContext.transitionWasCancelled)}return animator复制代码

animateTransition(using:)中调用上面的方法transitionAnimator(using:)

transitionAnimator(using: transitionContext).startAnimation()复制代码

LockScreenViewController中定义常量属性:

let presentTransition = PresentTransition()复制代码

LockScreenViewController遵守UIViewControllerTransitioningDelegate协议:

// MARK: - UIViewControllerTransitioningDelegateextension LockScreenViewController: UIViewControllerTransitioningDelegate {    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {    return presentTransition  }}复制代码

UIViewControllerTransitioningDelegate协议在 中学习过。

animationController(forPresented:presents:source:)方法是告诉UIKit,我想自定义视图控制器转场。

LockScreenViewController中,找到点击Edit按钮的ActionpresentSettings(_:),添加代码:

settingsController = storyboard?.instantiateViewController(withIdentifier: "SettingsViewController") as! SettingsViewControllersettingsController.transitioningDelegate = selfpresent(settingsController, animated: true, completion: nil)复制代码

运行,点击Edit按钮,SettingsViewController有点问题:

Main.storyboard中将视图的背景更改为Clear Color

运行,变成:

下面向动画师添加新属性,为了可以将任何自定义动画注入转场动画, 使用相同的转场类来生成略有不同的动画。

PresentTransition中添加两个新属性:

var auxAnimations: (() -> Void)?var auxAnimationsCancel: (() -> Void)?复制代码

transitionAnimator(using:)方法中动画师返回之前添加:

if let auxAnimations = auxAnimations {    animator.addAnimations(auxAnimations)}复制代码

这样可以根据具体情况在转换中添加自定义动画。 例如,要为当前转场添加模糊动画。

打开LockScreenViewController并在presentSettings()的开始处插入:

presentTransition.auxAnimations = blurAnimations(true)复制代码

再试一次过渡,看看这一行如何改变它:

模糊动画重复使用了。

另外,当用户解除控制器时,还需要隐藏模糊视图。

presentSettings(_:)中的present(_:animated:completion:)前添加:

settingsController.didDismiss = { [unowned self] in      self.toggleBlur(false)    }复制代码

现在,运行,点击SettingsViewController视图中的Cancel或其他选项,先有的模糊视图,然后恢复到第一个视图控制器:

交互视图控制器转场

这个部分通过下拉的手势来时学习实现交互视图控制器转场。

首先,让我们使用强大的UIPercentDrivenInteractionTransition类来启用视图控制器转场的交互性。

打开PresentTransition.swift把下面:

class PresentTransition: NSObject, UIViewControllerAnimatedTransitioning复制代码

替换为:

class PresentTransition: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning {复制代码

UIPercentDrivenInteractiveTransition是一个定义基于“百分比”的转场方法的类,例如有三个方法:

  • update(_:) 回退转场。
  • cancel() 取消视图控制器转场。
  • finish() 播放转场直到完成。

之前学习的中也提到相关内容。

UIPercentDrivenInteractiveTransition的一些属性:

  • timingCurve:如果以交互方式驱动转场,并且是播放转场时直到结束,就可以通过设置此属性为动画提供自定义时序曲线。

  • wantsInteractiveStart:默认是true,是否使用交互式转场。

  • pause() :调用此方法暂停非交互式转场并切换到交互模式。

PresentTransition添加一个新方法:

func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {    return transitionAnimator(using: transitionContext)  }复制代码

这是UIViewControllerAnimatedTransitioning协议的一个方法。 它允许我们UIKit提供可中断的动画师。

转场动画师类现在有两种不同的行为:

  1. 如果以非交互方式使用它(当用户按下编辑按钮时),UIKit将调用animateTransition(using:)来设置转场动画。
  2. 如果以交互方式使用它,UIKit将调用interruptibleAnimator(using:),获取动画师,并使用它来推动这种转场。

切换到LockScreenViewController.swift, 在UIViewControllerTransitioningDelegate扩展中添新方法:

func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {    return presentTransition}复制代码

接下来,在LockScreenViewController中添加两个新属性,用来跟踪用户的手势:

var isDragging = false  var isPresentingSettings = false复制代码

当用户向下拉时,将isDragging标志设置为true,当拉得足够远,也将将isPresentingSettings设置为true

实现UISearchBarDelegate的一个方法:

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {    isDragging = true}复制代码

这可能看起来有点多余,因为UITableView已经有一个属性来跟踪它当前是否被拖动,但现在要自己做一些自定义跟踪。

接下来继续实现UISearchBarDelegate协议的另一个方法,用来跟踪用户的进度:

func scrollViewDidScroll(_ scrollView: UIScrollView) {    guard isDragging else { return }    if !isPresentingSettings && scrollView.contentOffset.y < -30 {        isPresentingSettings = true        presentTransition.wantsInteractiveStart = true        presentSettings()        return    }}复制代码

接下来,需要添加代码以交互方式更新。 将以下内容追加到上面方法的末尾:

if isPresentingSettings {    let progess = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))    presentTransition.update(progess)}复制代码

根据拉出TableView的距离计算0.0到1.0范围内的进度,并在转场动画师上调用update(_:)以将动画定位到当前进度。 运行,当向下拖动时,将看到表格视图逐渐模糊。

还需要注意完成取消转场,实现UISearchBarDelegate协议的另一个方法:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer
) { let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0)) if progress > 0.5 { presentTransition.finish() } else { presentTransition.cancel() } isPresentingSettings = false isDragging = false}复制代码

这段代码看起来与中相似。如果用户下拉已经超过距离的一半,则认为转场成功;如果用户未下拉超过一半,则取消转场。

transitionAnimator(using:)方法中的addCompletion代码块替换为:

animator.addCompletion { (position) in      switch position {      case .end:        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)      default:        transitionContext.completeTransition(false)      }    }复制代码

运行,上下拉动,可能会出现下面这种像素化问题情况(iOS10可能会出现,iOS11之后应该修复了):

使用之前在PresentTransition中添加的auxAnimationsCancel属性。 在transitionAnimator(using:)中找到animator.addCompletion的调用,并在default:添加:

self.auxAnimationsCancel?()复制代码

LockScreenViewControllerpresentSettings(_:)方法。在设置auxAnimations属性后,添加:

presentTransition.auxAnimationsCancel = blurAnimations(false)复制代码

运行,像素化问题应该已经消失。

但是还有另一个问题。点击Edit按钮的非交互式转场没反应了!?

只要用户点击Edit按钮,就需要更改代码以将视图控制器转场设置为非交互式。

LockScreenViewControllertableView(_:cellForRowAt:),在self.presentSettings()之前插入:

self.presentTransition.wantsInteractiveStart = false复制代码

运行,效果:

可中断的转场动画

接下来,要考虑转场期间在非交互模式和交互模式之间切换。

在这一部分,将实现点击Edit按钮后开始执行显示设置控制器的动画,但如果用户在动画期间再次点击屏幕,则暂停转场。

切换到PresentTranstion.swift。需要稍微改变动画师,不仅要分别处理交互式和非交互式模式,还要同时处理相同的过渡。 在PresentTranstion中再添加两个属性:

var context: UIViewControllerContextTransitioning?var animator: UIViewPropertyAnimator?复制代码

使用这两个属性来跟踪动画的上下文以及动画师。 在transitionAnimator(using:)方法的return animator前插入:

self.animator = animatorself.context = transitionContext复制代码

每次为转场创建新的动画师时,也会存储对它的引用。

转场完成后释放这些资源也很重要。 继续添加:

animator.addCompletion { [unowned self] _  in  self.animator = nil  self.context = nil}复制代码

PresentTranstion中再添加一个方法:

func interruptTransition() {    guard let context = context else { return }    context.pauseInteractiveTransition()    pause()}复制代码

transitionAnimator(using:)方法的return animator前插入:

animator.isUserInteractionEnabled = true复制代码

确保转场动画是交互式的,这样用户可以在暂停后继续与屏幕进行交互。

允许用户向上或向下滚动以分别完成或取消转场。 为此,在LockScreenViewController中添加一个新属性:

var touchesStartPointY: CGFloat? 复制代码

如果用户在转场期间触摸屏幕,可以将其暂停并存储第一次触摸的位置:

override func touchesBegan(_ touches: Set
, with event: UIEvent?) { guard presentTransition.wantsInteractiveStart == false, presentTransition.animator != nil else { return } touchesStartPointY = touches.first!.location(in: view).y presentTransition.interruptTransition() }复制代码

跟踪用户触摸并查看用户是向上还是向下平移,添加:

override func touchesMoved(_ touches: Set
, with event: UIEvent?) { guard let startY = touchesStartPointY else { return } let currentPoint = touches.first!.location(in: view).y if currentPoint < startY - 40 { touchesStartPointY = nil presentTransition.animator?.addCompletion({ (_) in self.blurView.effect = nil }) presentTransition.cancel() } else if currentPoint > startY + 40 { touchesStartPointY = nil presentTransition.finish() }}复制代码

运行,点击Edit按钮后,立即点击屏幕,这个时候转场会暂停,此时向下滑动会完成转场,向上滑动会取消转场,效果如下:

本文在我的个人博客中地址:

转载地址:http://flzmx.baihongyu.com/

你可能感兴趣的文章
Java8之Stream
查看>>
响应式设计工具网站mydevice.io
查看>>
编程范式(Programming Paradigm)-[ 程序员的编程世界观 ]
查看>>
根据科目计算父科目ID,并递归累计求父科目的金额
查看>>
Dispatch Groups
查看>>
使用System.getProperty方法,如何配置JVM系统属性
查看>>
C# 正则表达式大全
查看>>
Jersey 写restful接口时QueryParam ,FormParam 等的区别
查看>>
Linux下IP SAN共享存储操作记录
查看>>
C# Word转为多种格式文件(Word转XPS/SVG/EMF/EPUB/TIFF)
查看>>
sublime 学习笔记
查看>>
Java NIO FileChannel
查看>>
PyCharm 默认运行 unittest
查看>>
使用web3j工具生成java版本的智能合约
查看>>
New UWP Community Toolkit - Carousel
查看>>
每天记录一点:NetCore获得配置文件 appsettings.json vue-router页面传值及接收值 详解webpack + vue + node 打造单页面(入门篇) 30...
查看>>
点击按钮,进入全屏与退出全屏切换
查看>>
在mysql命令行下执行sql文件
查看>>
【win7 + win server 2008】设置定时任务,设置.bat 文件去执行php脚本 == 用来配合爬虫程序简直不要太爽...
查看>>
php资源类型变量
查看>>