如何实现iOS图书动画-第2部分(下)

状态 2 - 打开书

现在,状态1的动画完成了,我们可以转移到状态2的处理中来。在这里我们将一本合起的书转换成一本打开的书。在setStartPositionForPush(_:toVC:)方法下添加如下方法:

func setEndPositionForPush(fromVC: BooksViewController, toVC: BookViewController) {
  //1
  for cell in fromVC.collectionView!.visibleCells() as! [BookCoverCell] {
    cell.alpha = 0
  }

  //2
  for cell in toVC.collectionView!.visibleCells() as! [BookPageCell] {
    cell.layer.transform = transforms[cell]!
    cell.updateShadowLayer(animated: true)
  }
}

上述代码解释如下:

  1. 隐藏所有书的封面,因为接下来我们要显示所选图书的内容。
  2. 在BookViewController中遍历书中每一页并读取先前保存在transform数组中的的Transform。

在从BooksViewController导航到BookViewController后,我们还需要进行一些清理工作。

在上面的方法之后加入如下方法:

func cleanupPush(fromVC: BooksViewController, toVC: BookViewController) {
  // Add background back to pushed view controller
  toVC.collectionView?.backgroundColor = toViewBackgroundColor
}

在Push完成时,我们将BookViewController的Collection View的背景色设回原来保存的颜色,隐藏位于它下面的内容。

实现打开书的动画

现在我们已经实现了助手方法,接下来要实现Push动画了!

在空的animateTransition(_:)方法中加入以下代码:

//1
let container = transitionContext.containerView()
//2
if isPush {
  //3
  let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BooksViewController
  let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BookViewController
  //4
  container.addSubview(toVC.view)

  // Perform transition
  //5
  self.setStartPositionForPush(fromVC, toVC: toVC)

  UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.7, options: nil, animations: {
    //6
    self.setEndPositionForPush(fromVC, toVC: toVC)
    }, completion: { finished in
      //7
      self.cleanupPush(fromVC, toVC: toVC)
      //8
      transitionContext.completeTransition(finished)
  })
} else {
  //POP
}

以上代码解释如下:

  1. 获取Container View,Container View在两个View Controller发生转场时充当父视图的角色。
  2. 判断当前的转场动作是否是一个Push动作。
  3. 如果是,分别获取fromVC(BooksViewController)和toVC(BookViewController)。
  4. 将toVC(BookViewController)加到Container View。
  5. 设定Push动作的起止点,即toVC和fromVC。
  6. 开始动画。从起始点(书合起的状态)转变到终点(书打开状态)。
  7. 执行清理动作。
  8. 告诉系统,转换完成。

在Navigation Controller中应用Push动画

现在我们已经创建好Push动画,接下来就是将它应用到自定义的Navigation Controller中了。

打开BooksViewController.swift在类声明中增加属性:

var transition: BookOpeningTransition?

transition属性用于保存Transition对象,通过它我们可以知道当前动画是Push动画还是Pop动画。

然后在文件末尾的大括号之后加入一个扩展:

extension BooksViewController {
func animationControllerForPresentController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  // 1
  var transition = BookOpeningTransition()
  // 2
  transition.isPush = true
  // 3
  self.transition = transition
  // 4
  return transition
  }
}

通过扩展,我们将一部分代码分离出来。这里,我们将和转换动画有关的方法放到了一起。上面的这个方法创建并返回了一个Transition对象。

以上代码解释如下:

  1. 创建一个新的Transition。
  2. 因为我们是要弹出或者Push一个Controller,所以将isPush设置为true。
  3. 保存当前Transition对象。
  4. 返回Transition对象。

现在打开CustomNavigationController.swift并将Push的if语句替换为:

if operation == .Push {
  if let vc = fromVC as? BooksViewController {
    return vc.animationControllerForPresentController(toVC)
  }
}

上述语句判断当前Push的View Controller是不是一个BooksViewController,如果是,用我们创建的BookOpeningTransition呈现BookViewController。

编译运行,选择某本书,你将看到书缓缓由合起状态打开:

呃…我们的动画效果呢?

书直接从合起状态跳到了打开状态,原因在于我们没有加载cell(书页)!

导航控制器从BooksViewController切换到BookViewController,这二者都是UICollecitonViewController。UICollectionViewCell没有在主线程中加载,因此代码一开始的时候以为cell的个数为0——这样当然不会有动画产生。

我们需要让Collection View有足够的时间去加载所有的Cell。

打开BooksViewController.swift将openBook(_:)方法替换为:

func openBook(book: Book?) {
  let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController
  vc.book = selectedCell()?.book
  //1
  vc.view.snapshotViewAfterScreenUpdates(true)
  //2
  dispatch_async(dispatch_get_main_queue(), { () -> Void in
    self.navigationController?.pushViewController(vc, animated: true)
    return
  })
}

以上代码解释如下:

  1. 告诉BookViewController在动画一开始之前截屏。
  2. 将Push BookViewController的动作放到主线程中进行,这样就有时间去加载cell了。

编译、运行,这次你将看到正确的Push动画了:

这样看起来是不是好多啦?

现在,关于Push动画的内容就到此结束,接下来,我们开始实现Pop动画。

实现Pop动画的助手方法

一个View Controller的Pop动作刚好和Push相反。状态1是图书打开的状态,而状态2则变成了书合起的状态:

Open up BookOpeningTransition.swift and add the following code:

打开BookOpeningTransition.swift,加入以下方法:

// MARK: Pop methods
func setStartPositionForPop(fromVC: BookViewController, toVC: BooksViewController) {
  // Remove background from the pushed view controller
  toViewBackgroundColor = fromVC.collectionView?.backgroundColor
  fromVC.collectionView?.backgroundColor = nil
}

setStartPositionForPop(_:toVC)方法仅仅是保存BookViewController的背景色并将BooksViewController的Collection View的背景色删除。注意,你不需要创建任何cell动画,因为书在这个时候是打开状态。

接着,在上面的方法后面加入这个方法:

func setEndPositionForPop(fromVC: BookViewController, toVC: BooksViewController) {
  //1
  let coverCell = toVC.selectedCell()
  //2
  for cell in toVC.collectionView!.visibleCells() as! [BookCoverCell] {
    if cell != coverCell {
      cell.alpha = 1
    }
  }
  //3
  for cell in fromVC.collectionView!.visibleCells() as! [BookPageCell] {
    closePageCell(cell)
  }
}

这个方法创建Pop动画的起止点,即从打开变成合起:

  1. 获取选择的书的封面。
  2. 在合起状态,在BooksViewController中遍历私有书的封面,然后对所有对象进行一个渐入效果。
  3. 在BookViewController中遍历当前图书的所有页,将所有cell转变成合起状态。

现在新建如下方法:

func cleanupPop(fromVC: BookViewController, toVC: BooksViewController) {
  // Add background back to pushed view controller
  fromVC.collectionView?.backgroundColor = self.toViewBackgroundColor
  // Unhide the original book cover
  toVC.selectedCell()?.alpha = 1
}

这个方法在Pop动画完成时执行清理动作:将BooksViewController的Collection View的背景色设回它开始的值并显示封面。

在animateTransition(_:)方法里面,找到注释有“//POP”的else语句块,添加如下代码:

//1
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BookViewController
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BooksViewController

//2
container.insertSubview(toVC.view, belowSubview: fromVC.view)

//3
setStartPositionForPop(fromVC, toVC: toVC)
UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: {
  //4
  self.setEndPositionForPop(fromVC, toVC: toVC)
}, completion: { finished in
  //5
  self.cleanupPop(fromVC, toVC: toVC)
  //6
  transitionContext.completeTransition(finished)
})

以上代码解释如下:

  1. 获取动画中涉及的两个ViewController。现在,fromVC 是BookViewController (打开状态),toVC是BooksViewController(合起状态)。
  2. 向Container View中加入BooksViewController(在BookViewContorller的下方)。
  3. setStartPositionForPop(_:toVC) 方法先保存背景色,再将背景色设为nil。
  4. 执行动画,即从打开状态切换到合起状态。
  5. 动画完成,执行清理动作。将背景色设回原来值,显示封面。
  6. 通知动画完成。

在Navigation Controller中应用Pop动画

现在需要创建Pop动画,就如同我们在Push动画所做一样。

打开BooksViewController.swift,在animationControllerForPresentController(_:)方法后增加如下方法:

func animationControllerForDismissController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  var transition = BookOpeningTransition()
  transition.isPush = false
  self.transition = transition
  return transition
}

这里,我们创建了一个新的BookOpeningTransition对象,但不同的是isPush设置为false。

打开CustomNavigationController.swift,然后替换Pop部分的if语句为:

if operation == .Pop {
  if let vc = toVC as? BooksViewController {
    return vc.animationControllerForDismissController(vc)
  }
}

上述代码返回一个Transition对象,并执行Pop动画,合起书本。

编译,运行程序,选择一本书,查看它的打开和合起。如下图所示:

创建互动式的Navigation Controller

打开和合起动画搞定了——但我们还能更进一步!我们为什么不用一个更直观的捏放手势来打开和合起书本呢?

打开BookOpeningTransition.swift,增加如下属性定义:

// MARK: Interaction Controller
var interactionController: UIPercentDrivenInteractiveTransition?

然后打开CustomNavigationController.swift,加入下列代码:

func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
  if let animationController = animationController as? BookOpeningTransition {
    return animationController.interactionController
  }
  return nil
}

在这个方法中,我们从BookOpeningTransition对象获得了一个interactionController。这样导航控制器能够跟踪动画进程以便用户可以用捏放手势打开和合起书。

打开BooksViewController.swift,在trnasitoin变量下增加如下属性:

//1
var interactionController: UIPercentDrivenInteractiveTransition?
//2
var recognizer: UIGestureRecognizer? {
  didSet {
    if let recognizer = recognizer {
      collectionView?.addGestureRecognizer(recognizer)
    }
  }
}

这两个属性的作用分别是:

  1. interactionController 是一个UIPercentDrivenInteractiveTransition类,它负责管理View Contorller之间转场的自定义动画。interactionController由一个Transition Animator生成,后者是一个实现了UIViewControllerAnimatorTransitioning协议的对象。而我们已经拥有了BookOpeningTransition——这就是一个实现了UIViewControllerAnimatorTransitioning的对象。interactionController能够控制Push动画和Pop动画之间的进度。关于这个类的更多内容,请参考Apple官方文档。
  2. recognizer 是一个UIGestureRecognizer。我们用这个手势识别器实现以捏放手势开合书本。

在BooksViewController扩展的animationControllerForPresentController(_:)方法中,transition.isPush=true一行下面,加入代码:

transition.interactionController = interactionController

这句代码让CustomNavigationController知道要用哪个interaction controller。

在animationControllerForDismissController(_:)方法中transition.isPush=false一行下面加入同样的代码:

transition.interactionController = interactionController

在viewDidLoad()方法中增加代码:

recognizer = UIPinchGestureRecognizer(target: self, action: "handlePinch:")

这里我们初始化了一个UIPinchGestureRecognizer,允许用户在做出捏放手势时调用handlePinch(_:)方法。

在viewDidLoad()方法下面实现这个方法:

// MARK: Gesture recognizer action
func handlePinch(recognizer: UIPinchGestureRecognizer) {
  switch recognizer.state {
    case .Began:
      //1
      interactionController = UIPercentDrivenInteractiveTransition()
      //2
      if recognizer.scale >= 1 {
        //3
        if recognizer.view == collectionView {
          //4
          var book = self.selectedCell()?.book
          //5
          self.openBook(book)
        }
      //6
      } else {
        //7
        navigationController?.popViewControllerAnimated(true)
      }
    case .Changed:
      //8
      if transition!.isPush {
        //9
        var progress = min(max(abs((recognizer.scale - 1)) / 5, 0), 1)
        //10
    interactionController?.updateInteractiveTransition(progress)
    //11
      } else {
        //12
    var progress = min(max(abs((1 - recognizer.scale)), 0), 1)
        //13
    interactionController?.updateInteractiveTransition(progress)
      }
    case .Ended:
      //14
      interactionController?.finishInteractiveTransition()
      //15
      interactionController = nil
    default:
      break
  }
}

对于UIPinchGestureRecognizer,我们要关注这3个状态:开始状态,这让你知道捏放手势何时开始;改变状态,检测捏放手势的变化;结束状态,让你知道捏放手势何时结束。

handlePinch(_:)方法代码解释如下:

开始状态

1. 创建一个UIPercentDrivenInteractiveTransition 对象。

2. scale取决于捏合点之间的距离,判断scale值是否大于或者等于1。

3. 如果是,判断相关的View是否是一个Collection View。

4. 获取正在被捏合的书。

5. 执行Push BookViewController的动画,显示书本中的书页。

6. 如果 scale 小于 1…

7. …执行Pop BookViewController的动画,显示封面

改变状态 – 捏合过程中

8. 判断当前是否是Push动画。

9. 如果正在Push一个BookViewConroller,计算捏放手势的进度。该进度必然是0-1之间的数字。我们将原始值除以5以让用户拥有更好的控制感。否则用双指打开的手势打开一本书时,会突然跳到打开状态。

10. 基于我们计算的进度,更新动画进度。

11. 如果当前不是Push动画,则它应该是Pop动画。

12. 当双指捏合合起一本书时,scale值必然是从1慢慢变到0。

13. 最后, 更新动画进度。

结束状态 – 手势终止

14. 告诉系统,用户交互式动画完成。

15.将interaction controller 设置为 nil。

最后,我们需要实现“捏合以合起书本”的状态。当然,我们必须将手势识别器传递给BookViewController以便它会Pop。

打开BookViewController.swift,在book变量声明下增加一个属性:

var recognizer: UIGestureRecognizer? {
  didSet {
    if let recognizer = recognizer {
      collectionView?.addGestureRecognizer(recognizer)
    }
  }
}

当我们将手势识别器传递给BookViewController时,它会被添加到Collection View,因此我们可以跟踪到用户的“关书”手势。

然后需要在BooksViewController和BookViewController之间传递手势识别器。

打开BookOpeningTransition.swift。在cleanUpPush(_:toVC)方法中,在设置背景色之后添加如下代码:

// Pass the gesture recognizer
toVC.recognizer = fromVC.recognizer

当我们从BooksViewController Push到BookViewController时,将捏放手势传递给BookViewController。这会导致捏放手势自动添加到Collection View中。

当我们从BookViewController Pop回BooksViewController时,我们必须将捏放手势又传递回去。

在cleanUpPop(_:toVC)方法中,在我设置背景色之后添加如下代码:

// Pass the gesture recognizer
toVC.recognizer = fromVC.recognizer

编译、运行程序,选择一本书,用捏放手势打开和合起书:

捏放手势是一种天然就适合用于对书本进行“开关”的手势;它让我们的界面显得更加简单。我们不再需要导航栏的Back按钮——因此我们决定去掉它。

打开Main.storyboard,选择Custom Navigation View Controller,打开属性面板,在Navigation Controller一栏下面,取消Bar Visibility选项,如下所示:

再次编译运行程序:

接下来做什么

你可以在这里下载到上面所有步骤完成后的最终项目。

在本教程中,我们学习如何对Collection View进行自定义布局,让App的用户体验更加自然、也更加有趣。我们还创建了自定义动画,使用智能交互让用户以捏放手势开合一本书。这个App在实现了所有基本功能的同时,让程序显得更加的人性化和与众不同。

相比较之下,是默认的“淡入/淡出”动画更简单一些。它能节省你一部分开发时间。但是杰出的应用程序都应当有一些自己特有的地方,从而使它们能够脱颖而出。

要知道,每个人都喜欢记住那些用起来非常有趣的App,在UI上能让人感到兴奋而同时又没有牺牲功能的App。

希望你能喜欢本教程,再次感谢Attila Hegedüs提供了这个教程的示例项目。

如果对本教程有任何问题,请加入到下面的讨论中来!

时间: 2024-08-13 18:19:00

如何实现iOS图书动画-第2部分(下)的相关文章

如何实现iOS图书动画-第2部分(上)

原文链接 : How to Create an iOS Book Open Animation: Part 2 原文作者 : Vincent Ngo 译文出自 : 开发技术前线 www.devtf.cn 译者 : kmyhy 欢迎回到iOS图书动画系列教程!在第一部分,我们学习了如何创建两个自定义的collection view layout并在图书书页中使用了阴影图层以使我们的App显得更加立体和真实. 在这一部分,我们将学习如何创建自定义的转场动画并通过捏放手势来打开一本书. 注意:感谢At

iOS图书动画

原文链接 : How to Create an iOS Book Open Animation: Part 2 原文作者 : Vincent Ngo 译文出自 : 开发技术前线 www.devtf.cn 转载声明: 本译文已授权开发者头条享有独家转载权,未经允许,不得转载! 译者 : kmyhy 欢迎回到iOS图书动画系列教程!在第一部分,我们学习了如何创建两个自定义的collection view layout并在图书书页中使用了阴影图层以使我们的App显得更加立体和真实. 在这一部分,我们将

[iOS]过渡动画之高级模仿 airbnb

注意:我为过渡动画写了两篇文章:第一篇:[iOS]过渡动画之简单模仿系统,主要分析系统简单的动画实现原理,以及讲解坐标系.绝对坐标系.相对坐标系,坐标系转换等知识,为第二篇储备理论基础.最后实现 Mac 上的文件预览动画.第二篇:[iOS]过渡动画之高级模仿 airbnb,主要基于第一篇的理论来实现复杂的界面过渡,包括进入和退出动画的串联.最后将这个动画的实现部分与当前界面解耦,并封装为一个普适(其他类似界面也适用)的工具类. 这两篇文章将会带你学到如何实现下图 airbnb 首页类似的过渡动画

iOS block-base 动画简单用法+关键帧动画设置线性变化速度的问题

本文转载至 http://www.tuicool.com/articles/aANBF3m 时间 2014-12-07 20:13:37  segmentfault-博客原文  http://segmentfault.com/blog/alan/1190000002411296 iOS的各种动画相漂亮,相信这是吸引很多人买iPhone的原因之一.不仅如此,这还是吸引我做iOS开发的一大原因,因为在iOS上给界面实现一些像样的动画实在是太轻松了! 这里就介绍一下iOS的block-based an

iOS核心动画

iOS开发系列--让你的应用“动”起来 --iOS核心动画 概览 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌.在这里你可以看到iOS中如何使用图层精简非交互式绘图,如何通过核心动画创建基础动画.关键帧动画.动画组.转场动画,如何通过UIView的装饰方法对这些动画操作进行简化等.在今天的文章里您可以看到动画操作在iOS中是如何简单和高效,很多原来想做但是苦于没有思路的动画在iOS中将变得越发简单: CALayer CALayer简介 CAL

iOS核心动画中的常用类型

CATransaction 当我们在自定义的图层上修改某些支持动画的属性时,系统会为该属性的修改自动产生动画.这种其实属于隐式动画.隐式动画要得益于CATransaction. 一个CATransaction从调用CATransaction.begin()开始,以CATransaction.commit()结束.在这其间对图层属性的修改,会受该Transaction的控制,可以通过setAnimationDuration修改Transaction的duration. 系统的隐式动画是因为在Run

iOS核心动画学习整理

最近利用业余时间终于把iOS核心动画高级技巧(https://zsisme.gitbooks.io/ios-/content/chapter1/the-layer-tree.html)看完,对应其中一些知识做了相应的整理,整理为demo(https://github.com/PurpleSweetPotatoes/Layer_learn).此demo中都是基于教程书籍中的编程示例,并加上了注解以方便各位iOS爱好者学习使用. 在这里利用此教程中的基础知识做了2个小demo,活动指示器效果和火焰效

iOS开发——动画OC篇&所有常用动画总结

所有常用动画总结 先来装下B,看不懂没关系,其实我也看不懂-?? iOS provides several different frameworks for adding graphics and animations to your apps. UIKit is an Objective-C API that provides basic 2D drawing, image handling, and ways to animate user interface objects. Core G

iOS核心动画高级技巧之核心动画(三)

iOS核心动画高级技巧之CALayer(一) iOS核心动画高级技巧之图层变换和专用图层(二)iOS核心动画高级技巧之核心动画(三)iOS核心动画高级技巧之性能(四)iOS核心动画高级技巧之动画总结(五) 隐式动画 隐式动画主要作用于CALayer的可动画属性上面,UIView对应的layer是不可以的,只要你改变属性的值,它不是突兀的直接改变过去,而是一个有一个动画的过程,这个时间等属性你可以通过事务(CATransaction)来控制,如果你不自己提供一个事务,它的默认时间是0.25秒,当然