国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > php开源 > 综合技术 > iOS 动画教程-自定义 View Controller 呈现转换

iOS 动画教程-自定义 View Controller 呈现转换

来源:程序员人生   发布时间:2017-04-14 16:48:49 阅读次数:6777次

原文:ios-animation-tutorial-custom-view-controller-presentation-transitions
作者:Marin Todorov
译者:kmyhy

当你显现相机、通讯录、或某种自定义模式窗口时,你每次都会调用同1个 UIKit 方法 present(_:animated:completion:)。这个方法将当前屏幕“让给”另外一个 view controller。

默许的显现动画简单地用新视图推开当前视图。下图演示了“新建联系人” view controller 在联系人列表视图上层向上滑出:

在本教程中,你将用自己的自定义显现动画替换默许动画,并完本钱教程中的项目。

开始

下载本文的开始项目 Beginner Cook。打开 Main.storyboard :

第1个 view controller(即 ViewController)包括了 app 的标题和主要介绍和底部的1个 scroll view,用于显示1个有用的香草列表。

当用户点击了列表中的图片,main view controller 会显现1个 HerbDetailsViewController;这个 view controller 有1个背景、1个标题、1个描写和几个按钮用于注明图片的所有者。

在 ViewController.swift 和 HerbDetailsViewController.swift 已有部份代码了,足以保持 app 运行。运行程序,app 是这个模样:

点击某个香草图片,细节页面以标准的弹出动画方式显现。对1般的 app 来讲这也足够了,但对你的 app 则需要做得更好!

你的任务是创建自定义显现动画让你的 app 更加残暴夺目!你需要将目前内置的动画替换成:用所点击的香草的图片展开至全屏!

撸起手袖,系紧围裙,准备动手开始定制显现控制器!

自定义动画的幕后工作

UIKit 允许你通过拜托模型定制化 view controller 的显现进程;你可让 main view controller(或可以用另外一个类专门来干这个)采取 UIViewControllerTransitioningDelegate 协议。

每当你显现1个新的 view controller 时,UIKit 会询问它的拜托是不是需要使用自定义动画。自定义动画的第1步是这样的:

UIKit 会调用 animationController(forPresented:presenting:source:) 方法,看是不是有1个 UIViewControllerAnimatedTransitioning 对象返回。如果这个方法返回空,UIKit 使用默许动画,否则,UIKit 使用返回的对象作为这次转换的动画控制器。

UIKit 首先询问动画控制器(简称为 animator),动画需要几秒钟?然后调用它的animateTransition(using:) 方法。这时候你的自定义动画开始生效了。

在 animateTransition(using:) 方法中,你可以同时访问到正在显示的 view controller 和行将显现的新 view controller。你可以淡入、缩放、旋转并为所欲为地操作已有的视图和新的视图。

你已大致了解了之定义显现控制器是如何工作的了,现在,开始来创建我们自己的吧!

实现 Transition 拜托

由于拜托的责任是管理动画控制器 animator 对象,而由 animator 来履行真实的动画,因此在编写拜托代码之前的第1件事情就是创建1个 animator 类。

打开 Xcode 菜单 File\New\File… 选择模板: iOS\Source\Cocoa Touch Class。
类名设置为 PopAnimator,语言选择 Swift,继承于 NSObject。
打开 PopAnimator.swift 修改类定义,实现 UIViewControllerAnimatedTransitioning 协议:

class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {

}

Xcode 会抱怨没有实现必须的拜托方法,等下我们会解决这个。
在类中添加以下方法:

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

动画时长返回 0 只是临时的;后面会修改这个为真实的时长。
继续新增以下方法:

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

}

这个方法将放入动画代码,暂时是空实现,以消除 Xcode 的报错。

有了基本的 animator 类以后,你可以在 view controller 中实现拜托方法了。
打开 ViewController.swift 新增以下扩大:

extension ViewController: UIViewControllerTransitioningDelegate {

}

这声明对 transitioning delegate 协议的实现。等会我们再来添加这些方法。

找到 didTapImageView(_:) 方法。在方法底部,你看到了显现详情 view controller 的代码。herbDetails 是新 view controller 的实例;你需要将它的 transitioning 拜托设置为 main controller。

在这个方法最后1行,即调用 present(…) 方法以后加入以下代码:

// ...
present(herbDetails, animated: true, completion: nil)
herbDetails.transitioningDelegate = self // 加入这行

现在 UIKit 会在每次显现 details view controller 的时候都索要1个 animator 对象。但你还没有实现任何 UIViewControllerTransitioningDelegate 方法,所以 UIKit 还是会使用默许的动画。

接下来应当实例化1个 animator 对象并在 UIKit 询问的时候返给它。
在 ViewController 中添加1个属性:

let transition = PopAnimator()

这个是1个 PopAnimator 对象,用于驱动你的 view controller 动画。你只需要1个 PopAnimator 对象,由于你可以在每次显现 view controller 时都使用同1个 animator 对象,由于每次的动画都是同1个。

在 ViewController 的扩大中加入第1个拜托方法:

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  return transition
}

这个方法提供几个参数,你可以根据它们来决定是返回1个自定义的动画还是不。在本文中,你总是返回同1个 PopAnimator 实例,由于你只有1个显现动画。

你已添加了1个用于显现 view controller 的拜托方法,那末用于解散的呢?

这是另外一个拜托方法:

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  return nil
}

这个方法和前1个方法基本是干同1件事情:判断要解散的是哪一个 view controller,觉此来决定是不是返回 nil,返回 nil 表示采取默许的解散动画,或返回1个自定义的 animator。这里你返回的是 nil,由于你还没有实现解散动画。

你已具有了1个自定义的 animator 来负责自定义动画,但它是如何工作的呢?

运行程序,点击任何1张香草图片:

甚么也没产生。为啥?你有1个用于驱动动画的自定义 animator,但是……等等,在 animator 类中还没有编写代码!你会在下1节完成这个任务。

创建 Transition Animator

打开 PopAnimator.swift; 这里我们将加入两个 view controller 之间进行转换的代码。
首先,加入几个属性:

let duration = 1.0
var presenting = true
var originFrame = CGRect.zero

duration 变量会用到几个地方,比如告知 UIKit 动画时长,和创建动画时。
我们还定义了1个 presenting 变量,用于告知 animator 类,当前是在显现还是解散进程。我们需要记住这个变量,由于我们将以正面顺序履行显现,而以相反顺序履行解散。

最后,我们用 originFrame 变量保存原来用户所点到的图片的 frame 形状——我们会将图片由这个 frame 以动画方式放大到全屏,反过来则履行相反动作。当你获得当前所选图片并将它的 frame 传递给 animator 实例时,需要注意这个 originFrame。

现在,你可以回到 UIViewControllerAnimatedTransitioning 方法来了。

在 transitionDuration() 方法中,用下句替换:

return duration

重用 duration 属性,能让你很容易可以调试 transition 动画。你可以简单修改这个值,使动画变快变慢。

设置转换上下文

现在为 animateTransition 注入魔力。这个方法有1个 UIViewControllerContextTransitioning 参数,通过它你能访问和转换相干的 view controller 和参数。

在开始编写代码之前,1个重要的问题就是理解 animation context 的实际上是甚么。

当两个 view controller 之间开始转换时,原来的 view 被添加到 transition container 转换容器,新的 view controller 的 view 被创建出来,但依然不可见,以下图所示:

因此你的任务是在 animateTransition() 方法中将新的 view 添加到转换容器,“以动画方式”显示它,如果有必要的话,将原本的 view 以动画方式移除。

默许,当转换动画完成时,原有 view 从转换容器中移除。

在你能够“烹制”出更多的食品之前,你需要创建1个简单的动画,看看它是如何实现的,然后再实现更酷的、同时也是更复杂的转换。

添加淡入动画

开始,我们用1个简单的淡出动画来实现自定义动画。在 animateTransition 方法中加入:

let containerView = transitionContext.containerView

let toView = transitionContext.view(forKey: .to)!

首先,获得容器 view,你的动画将在这个 view 中产生。然后获得新的 view 并赋给 toView 变量。
转换上下文有两个非常方便的方法,允许你访问动画的参与者

  • view(forKey:): 你可以访问到 “原本的” and “新的” view,通过指定 key 参数为 UITransitionContextViewKey.from 或 UITransitionContextViewKey.to。
  • viewController(forKey:): 你可以通过指定 key 参数为 UITransitionContextViewControllerKey.from 或 UITransitionContextViewControllerKey.to 来访问 “原本的” 和 “新的” view controller。

这里,你要同时用到 container view 和要显现的 view 。然后你将要显现的 view 添加为 container view 的 subview 并以某种方式动画。
在 animateTransition() 中添加:

containerView.addSubview(toView)
toView.alpha = 0.0
UIView.animate(withDuration: duration, 
  animations: {
    toView.alpha = 1.0
  }, 
  completion: { _ in
    transitionContext.completeTransition(true)
  }
)

注意,你需要在动画完成块中调用转换上下文的 completeTransition() 方法,这是为了通知 UIKit 你的转换动画已完成,UIKit 可以完成这次 view controller 转换了。

运行程序,点击某张香草图片,你会看到香草的介绍以淡入的方式出现在主 view controller 中:

这个转换委曲过得去,你已大概弄清了在 animateTransition 方法中应当干些甚么——你将在里面加入1些更好的东西!

加入1个 Pop 动画

新的动画需要重新调剂1下代码结构,因此将 animateTransition() 中的代码替换为:

let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let herbView = presenting ? toView : 
  transitionContext.view(forKey: .from)!

containerView 是你的动画行将产生的地方,toView 是需要显现的新视图。如果你正在显现,herbView 就是 toView,否则它应当从上下文中获得。对解散和显现,herbView 都会是你将履行动画的 view。

当你显现细节页面时,它会拉伸到全部屏幕大小。解散时,它又会缩小到原始 frame 大小。

在 animateTransition() 添加:

let initialFrame = presenting ? originFrame : herbView.frame
let finalFrame = presenting ? herbView.frame : originFrame

let xScaleFactor = presenting ?

  initialFrame.width / finalFrame.width :
  finalFrame.width / initialFrame.width

let yScaleFactor = presenting ?

  initialFrame.height / finalFrame.height :
  finalFrame.height / initialFrame.height

在上述代码中,我们需要根据条件取得本来的 frame 和终究动画结束时的 frame,然后计算两个 view 之间的横纵比例。

现在我们需要关注新 view 的位置,由于它需要显示在所点击的 image 上方,看起来就像是被点的图象拉伸到了全屏大小。
在 animateTransition() 中添加:

let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, 
                                            y: yScaleFactor)

if presenting {
  herbView.transform = scaleTransform
  herbView.center = CGPoint(
    x: initialFrame.midX,
    y: initialFrame.midY)
  herbView.clipsToBounds = true
}

当显现新 view 时,我们设置了它的 scale 和位置以便和原图 frame 的位置大小匹配。
现在在 animateTransition() 中加入最后的代码:

containerView.addSubview(toView)
containerView.bringSubview(toFront: herbView)

UIView.animate(withDuration: duration, delay:0.0, 
  usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0,
  animations: {
    herbView.transform = self.presenting ?
      CGAffineTransform.identity : scaleTransform
    herbView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
  }, 
  completion:{_ in
    transitionContext.completeTransition(true)
  }
)

首先将 toView 添加到 container。然后,让 herbView 放在 subview 的最上层,由于你只会对这个 view 进行动画。记住,在解散时,toView 是原始 view,因此在第1个行代码,你会将 toView 加在最上层,这样你的动画会被隐藏在下层看不见,所以你需要将 herbView 放到上层。

然后,开始动画。这里使用了1个 spring 动画,这会带来1种弹簧效果。

在 animations 块中,我们修改 herbView 的 transform 属性和位置。在显现时,你将底部的小尺寸动画到全屏,因此目标 transform 就是 identity transform。在解散时,你将它的大小缩小到原始图片大小。

这里,我们已准备好了将新 view 的位置对齐被点到的图片,在原来的 frame 和终究的 frame 之间进行动画,最后调用 completeTransition() 方法将控制转给 UIKit。让我们来看看代码的实际效果!

运行程序,点击第1个香草图片,看看你的动画效果:

是的,它还不是10分完善。但当你修改了这些瑕疵,你的动画就会同你想象的1模1样!

当前你的动画是从左上角开始;由于 originFrame 默许的 origin 是(0,0)——你并没有修改过这个值。

打开 ViewController.swift 在 animationController(forPresented:) 头部加入:

transition.originFrame = 
selectedImage!.superview!.convert(selectedImage!.frame, to: nil) 
transition.presenting = true
selectedImage!.isHidden = true

将 transition 的 originFrame 设置为 selectedImage 即你刚刚点击的图片的 frame。然后将 presenting 设置为 true,在动画期间隐藏所选图片。

运行程序,点击列表中的不同香草,你会看到:

添加解散动画

剩下来的事情就是解散详情页面。实际上大部份工作都已在 animator 中做完了——转换动画中的代码中开始、结束 frame 都已设置正确,你最后的工作就是在显现和解散时适时地播放动画。开心吧?

打开 ViewController.swift 修改 animationController(forDismissed:) 方法为:

transition.presenting = false
return transition

这将告知 animator 对象,你将解散1个 view controller,这样动画代码会以正确的方式进行。
运行程序,点击1张香草图片然后点击屏幕任何地方解散它:

转换动画看起来没甚么问题,但请注意,你选择的香草从 scroll view 中消失了!当你解散细节页面时,你需要让所点击的图片重新显示。
打开 PopAnimator.swift 添加1个新的闭包属性:

var dismissCompletion: (()->Void)?

这将允许解散动画完成时履行你传入的代码。
然后,找到 animateTransition() 方法在完成块中,在调用 completeTransition() 之前加入:

if !self.presenting {
  self.dismissCompletion?()
}

当解散完成,调用 dismissCompletion —— 这里恰好可以显示原来的图片。
打开 ViewController.swift 在 viewDidLoad() 中加入:

transition.dismissCompletion = {
  self.selectedImage!.isHidden = false
}

这里当转换动画完成重新显示了原来的图片,以替换详情页面。

运行程序,体验转换动画,包括显现和解散。现在,香草不会在平白无故消失了!

装备方向的转换

注意: 这部份内容是可选的。如果你对装备方向改变不感兴趣的话,请跳到挑战部份。

你可以将装备方向改变看成是1种显现,从1个 view controller 转换到它自己,仅仅是 size 不同。

iOS 8 中出现的 viewWillTransition(to size:coordinator:)方法,允许你以1种简单直白的方式处理装备方向的变化。你不再需要为横屏竖屏分别设计不同的布局,相反,你只需改变 view controller 的视图 size。
打开 ViewController.swift ,实现 viewWillTransition(to:with:) 方法:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  super.viewWillTransition(to: size, with: coordinator)

}

第1个参数 size 通知你 view controller 当前正在转换到哪一个 size。第2个参数 coordinator 是1个 transition coordinator 对象,通过它可以访问该转换的许多属性。

当横屏的时候,你需要做的仅仅是下降 app 背景图片的 alpha 值,提高文字的可读性。

在 viewWillTransitionToSize 加入:

coordinator.animate(
  alongsideTransition: {context in
    self.bgImage.alpha = (size.width>size.height) ? 0.25 : 0.55
  }, 
  completion: nil
)

animate(alongsideTransition:) 允许你在旋屏进程中同时履行你指定的动画,也就是在 UIKit 履行默许旋屏动画的同时。
你的动画块会收到1个 transitionging 上下文,这和你在显现 view controller 时使用的上下文是1样的。这里,你没有 from 和 to 视图控制器了,由于它们是同1个,但你可以取得比如动画时长等属性。

在动画块中,我们判断目标 size 的宽度是不是大于高度,如果是,下降背景图的 alpha 值为 0.25。这将使横屏下的背景变淡。如果是竖屏模式,alpha 值设为 0.55。

运行程序,旋转装备(如果是摹拟器,按 command+左箭头),查看实际效果。

你将看到当旋转到横屏时背景变暗。这使得长文本更容易浏览。

如果你点击图片,你会注意到动画有点乱。由于屏幕旋转为横屏后,图片依然是竖向的大小。在原始图片和拉伸至全屏的图象之间的转换其实不流畅。

不要担心——你有1个新方法 viewWillTransition(to:with:) 能够解决这个问题。
ViewController 有1个成员方法叫 positionListItems(),它负责香草图片的大小和位置。这个方法在 app 1启动时,被 viewDidLoad()方法所调用。

在 animate(alongsideTransition:) 方法的动画块中,在设置 alpha 值以后加入以下代码:

self.positionListItems()

这将在装备旋转后改变香草图片的 size 和位置。当屏幕完成旋转后,香草图片也会被重新改变大小:

由于这些图片都已有了1个横屏布局,因此你的转换动画就可以正常运行了。试试看!

结束语

从这里下载终究完成的项目。

这里,你可以对这个转换进行大量的改进。例如,这些点子:

  • 在转换期间隐藏被点击的图片,以便它们看起来真的想“长大”到全部屏幕。
  • 让每一个香草的描写文本以淡入淡出的方式动画,这样转换动画会更加平滑。
  • 针对横屏对转换进行测试和调剂。
    如果你想学习更多内容,请参考我们的iOS Animations by Tutorials。这本书已完全迟滞 Swift 3 和 iOS10 。你会学习如何使用 spring 动画、转换、关键帧动画、CALayer 动画、自动布局束缚动画、view controller 转换动画等等!

希望你喜欢本教程,如果有任何问题和建议,请在下面留言!

生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生