系统学习iOS动画之五:使用UIViewPropertyAnimator

栏目: IOS · 发布时间: 5年前

内容简介:本文是我学习文中详细代码都放在我的Github上

本文是我学习 《iOS Animations by Tutorials》 笔记中的一篇。

文中详细代码都放在我的Github上 andyRon/LearniOSAnimations

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

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

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

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

内容预览:

20-UIViewPropertyAnimator入门

21-深入UIViewPropertyAnimator

22-用UIViewPropertyAnimator进行交互式动画

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

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

20-UIViewPropertyAnimator入门

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

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

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

基础动画

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

开始项目 已经实现了一些与动画无关的功能。 例如,如果点击 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:...) 中的 option 是类似的。

添加动画

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

为什么第二个参数不是一个简单的秒数值?

想象动画师已经在运行了,你决定在中途添加一些新的动画。 在这种情况下,剩余持续时间不会等于总持续时间,因为自启动动画以来已经过了一段时间。

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

添加完成闭包

viewDidAppear(_:) 中添加:

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

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

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

scale.startAnimation()

提取动画

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

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

import UIKit

class 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()
}

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

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

运行动画师

当用户使用搜索栏时,将淡入模糊图层(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()
  }
}

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

运行,效果:

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

基础关键帧动画

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

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善意的提醒:blush:。

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

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()
}

效果:

系统学习iOS动画之五:使用UIViewPropertyAnimator

提取模糊动画

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

@discardableResult
static 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)
}

防止动画重叠

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

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

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

var animator: UIViewPropertyAnimator?

func iconJiggle() {
  if let animator = animator, animator.isRunning {
    return
  }

  animator = AnimatorFactory.jiggle(view: icon)
}

对比可以发现有所不同:

系统学习iOS动画之五:使用UIViewPropertyAnimator

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()
}

运行,效果:

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

贝塞尔曲线

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

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

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

贝塞尔曲线是什么?

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

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

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

网上找了一个动图:

系统学习iOS动画之五:使用UIViewPropertyAnimator

具有一个控制点的Bézier曲线称为 二次曲线 。有两个控制点的Bézier曲线叫做 三次曲线 (立方贝塞尔曲线)。

我们使用的内置曲线就是三次曲线。

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

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

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

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 两个点,就是我们自定义三次曲线的控制点。

可以通过 http://cubic-bezier.com 网站来选着控制点。

弹簧动画

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

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

自定义时间曲线

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

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

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

阻尼和速度

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

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)

上面这些参数的工作原理,可以查看之前的文章11-图层弹簧动画。

自动布局动画

前面的文章 系统学习iOS动画之二:自动布局动画 学习了自动布局动画。

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

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

@discardableResult
static 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 -= 100
view.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 = showsMore
widgetView.reload()

系统学习iOS动画之五:使用UIViewPropertyAnimator

视图过渡

在视图动画的3-过渡动画,学习了视图过渡。现在用 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)

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

效果也可以尝试 .transitionFlipFromTop

22-用UIViewPropertyAnimator进行交互式动画

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

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

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

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

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

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

动画状态机

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

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

系统学习iOS动画之五:使用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 ,一旦动画师完成了动画的运行而不是自动停止,而是暂停。 这将使我们有机会在暂停状态下继续使用它。

状态流程图:

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

交互式3D touch动画

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

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

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

放慢的效果为:

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

为了让您开始开发动画本身, UIPreviewInteractionDelegate 方法已经连接到LockScreenViewController上调用相关方法。

WidgetView中的代码如下:

LockScreenViewController.startPreview(for:)
LockScreenViewController.updatePreview(percent:)
LockScreenViewController.finishPreview()
LockScreenViewController.cancelPreview()

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

var startFrame: CGRect?
var previewView: UIView?
var previewAnimator: UIViewPropertyAnimator?

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

previewView 图标的快照视图,动画期间暂时使用它。

previewAnimator 将成为驱动预览动画的交互式动画师。

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

let previewEffectView = IconEffectView(blur: .extraLight)

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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 视图,以防万一在屏幕上留下之前的视图。 然后,您可以创建集合视图图标的快照,最后将其添加到模糊效果视图上方的屏幕上。

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

previewView?.frame = forView.convert(forView.bounds, to: view)
startFrame = previewView?.frame
addEffectView(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范围内,因为我不希望在动画才这段结束,我另外指定的方法完成或取消动画。

运行,效果(放慢):

系统学习iOS动画之五:使用UIViewPropertyAnimator

你会(惊喜!)需要更多的动画师。 打开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 ,如果图标位于屏幕左侧,则显示右侧菜单,如果图标位于左侧,则显示左侧菜单在屏幕的右侧。请参阅以下差异:

系统学习iOS动画之五:使用UIViewPropertyAnimator

动画师准备好了,所以打开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()

运行,按压图标:

系统学习iOS动画之五:使用UIViewPropertyAnimator

关闭模糊视图

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

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

blurView.effect = UIBlurEffect(style: .dark)
blurView.isUserInteractionEnabled = true
blurView.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()
}

交互式关键帧动画

在20-UIViewPropertyAnimator入门学习了 用 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)
        })

    })
}

第一个关键帧运行您之前的相同动画。

第二个关键帧是简单旋转,效果:

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

系统学习iOS动画之四:视图控制器的转场动画 中,学习了如何创建自定义视图控制器转场。这个章节学习使用 UIViewPropertyAnimator 来自定义视图控制器转场。

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

静态视图控制器转场

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

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

import UIKit

class PresentTransition: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.75
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    }
}

UIViewControllerAnimatedTransitioning 协议已经在 系统学习iOS动画之四:视图控制器的转场动画 中学过。

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

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: - UIViewControllerTransitioningDelegate
extension LockScreenViewController: UIViewControllerTransitioningDelegate {
  
  func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return presentTransition
  }
}

UIViewControllerTransitioningDelegate 协议在 系统学习iOS动画之四:视图控制器的转场动画 中学习过。

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

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

settingsController = storyboard?.instantiateViewController(withIdentifier: "SettingsViewController") as! SettingsViewController
settingsController.transitioningDelegate = self
present(settingsController, animated: true, completion: nil)

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

运行,变成:

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

PresentTransition 中添加两个新属性:

var auxAnimations: (() -> Void)?
var auxAnimationsCancel: (() -> Void)?

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

if let auxAnimations = auxAnimations {
    animator.addAnimations(auxAnimations)
}

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

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

presentTransition.auxAnimations = blurAnimations(true)

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

模糊动画重复使用了。

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

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

settingsController.didDismiss = { [unowned self] in
  self.toggleBlur(false)
}

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

交互视图控制器转场

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

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

打开 PresentTransition.swift 把下面:

class PresentTransition: NSObject, UIViewControllerAnimatedTransitioning

替换为:

class PresentTransition: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning {

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

update(_:)
cancel()
finish()

之前学习的 19-交互式导航控制器转场 中也提到相关内容。

UIPercentDrivenInteractiveTransition 的一些属性:

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

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

PresentTransition 添加一个新方法:

func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
  return transitionAnimator(using: transitionContext)
}

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

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

animateTransition(using:)
interruptibleAnimator(using:)

系统学习iOS动画之五:使用UIViewPropertyAnimator

切换到 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(_:) 以将动画定位到当前进度。

运行,当向下拖动时,将看到表格视图逐渐模糊。

系统学习iOS动画之五:使用UIViewPropertyAnimator

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

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    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
}

这段代码看起来与 19-交互式导航控制器转场 中相似。如果用户下拉已经超过距离的一半,则认为转场成功;如果用户未下拉超过一半,则取消转场。

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

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

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

系统学习iOS动画之五:使用UIViewPropertyAnimator

使用之前在 PresentTransition 中添加的 auxAnimationsCancel 属性。

transitionAnimator(using:) 中找到 animator.addCompletion 的调用,并在 default: 添加:

self.auxAnimationsCancel?()

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

presentTransition.auxAnimationsCancel = blurAnimations(false)

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

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

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

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

self.presentTransition.wantsInteractiveStart = false

运行,效果:

系统学习iOS动画之五:使用UIViewPropertyAnimator

可中断的转场动画

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

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

切换到 PresentTranstion.swift 。需要稍微改变动画师,不仅要分别处理交互式和非交互式模式,还要同时处理相同的过渡。

PresentTranstion 中再添加两个属性:

var context: UIViewControllerContextTransitioning?
var animator: UIViewPropertyAnimator?

使用这两个属性来跟踪动画的上下文以及动画师。

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

self.animator = animator
self.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<UITouch>, 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<UITouch>, 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 按钮后,立即点击屏幕,这个时候转场会暂停,此时向下滑动会完成转场,向上滑动会取消转场,效果如下:

系统学习iOS动画之五:使用UIViewPropertyAnimator


以上所述就是小编给大家介绍的《系统学习iOS动画之五:使用UIViewPropertyAnimator》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JSP 应用开发详解(第二版)

JSP 应用开发详解(第二版)

飞思科技产品研发中心 / 电子工业出版社 / 2004-1-1 / 55.00

本书结合JSP和Servlet的最新规范,从基本的语法和规范入手,以经验为后盾,以实用为目标,以实例为导向,以实践为指导,深入浅出地讲解了JSP 开发中的种种问题。以精简的内容介绍了JSP的语法、Servlet技术、JDBC技术、标签库技术、表达式语言、Struts、JavaServer Face等技术;对于JSP开发中常遇到的典型的难点,专门设置了专题进行集中讨论。本书的最后一篇围绕一个电子商务......一起来看看 《JSP 应用开发详解(第二版)》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试