如何实现iOS图书动画:第1部分(上)

来源:互联网 发布:java如何 私有构造方法 编辑:程序博客网 时间:2024/05/30 23:39

如何实现iOS图书动画:第1部分

  • 原文链接 : How to Create an iOS Book Open Animation: Part 1
  • 原文作者 : Vincent Ngo
  • 译文出自 : 开发技术前线 www.devtf.cn
  • 译者 : kmyhy

本教程分为2个部分,教你开发一个漂亮的iOS图书打开和翻页动画,就像你在Paper 53中所见到的一样:

在第1部分,你将学习到如何定制化Collection View Layout,并通过使用深度和阴影使App看起来更真实。

在第2部分,你将学习如何以一种合理的方法在两个不同的控制器之间创建自定义的过渡特效,以及利用手势在两个视图间创建自然的、直观的过渡效果。

本教程适用于中级-高级的开发者;你将使用自定义过渡动画和自定义Collection View Layout。如果你从来没有用过Colleciton View,请先参考其他iOS教程。

注意:感谢Attila Hegdüs创建了本教程中的示例项目。

开始

从此处下载本教程的开始项目;解开zip压缩包,用Xcode打开Paper.xcodeproj。

编译项目,在模拟器中运行App;你将看到如下画面:

这个App的功能已经很完善了,你可以在你的书库中滚动,查看图书,选中某本图书进行浏览。但当你读一本书的时候,为什么它的书页都是并排放置的?通过一些UICollectionView的知识,你可以让这些书页看起来更好一些!

项目结构

Here’s a quick rundown of the most important bits of the starter project:

关于这个开始项目,有几个重要的地方需要解释:

Data Models文件夹包含3个文件:

  • Books.plist 中包含了几本用于演示的图书信息。每本图书包含一张封面图片,以及一个表示每一页的内容的图片的数组。
  • BookStore.swift实现了单例,在整个App声明周期中只能创建一次对象。BookStore的职责是从Books.plist中加载数据并创建Book类实例。
  • Book.swift用于存放图书相关信息的类,比如图书的封面,每一页的图片,以及页号。

Books文件夹包含了两个文件:

  • BooksViewController.swift是一个UICollectionViewController子类。负责以水平方式显式图书列表。
  • BookCoverCell.swift负责显示图书的封面,这个类被BooksViewController类所引用。

在Book文件夹中则包括:

  • BookViewController.swift也是UICollectionViewController的子类。当用户在BooksViewController中选定的一本书后,它负责显示图书中的书页。
  • BookPageCell.swift被BookViewController用于显示图书中的书页。

在最后一个文件夹Helper中包含了:

  • UIImage+Helpers.swift是UIImage的扩展。该扩展包含了两个实用方法,一个用于让图片呈圆角显示,一个用于将图片缩放到指定大小。

这就是整个开始项目的大致介绍——接下来该是我们写点代码的时候了!

定制化图书界面

首先我们需要在BooksViewController中覆盖Collection View的默认布局方式。但当前的布局是在屏幕上显示3张图书封面的大图。为了美观,我们将这些图片缩减到一定大小,如下图所示:

当我们滑动图片,移动到屏幕中心的图片将被放大,以表示该图书为选中状态。如果继续滑动,该图书的封面又会缩小到一边,表示我们放弃选择该图书。

在App\Books文件夹下新建一个文件夹组:Layout。在Layout上点击右键,选择New File…,然后选择iOS\Source\Cocoa Touch Class模板,并点击Next。类名命名为BooksLayout,继承UICollectionViewFlowLayout类,语言设置为Swift。

然后需要告诉BooksViewController中的Collection View,适用我们新建的BooksLayout。

打开Main.storyboard,展开BooksViewController对象,然后选择Collection View。在属性面板中,设置Layout 属性为 Custom,设置Class属性为BooksLayout,如下图所示:

打开BooksLayout.swift,在BooksLayout类声明之上加入以下代码:

private let PageWidth: CGFloat = 362private let PageHeight: CGFloat = 568

这个两个常量将用于设置单元格的的大小。
现在,在类定义内部定义如下初始化方法:

required init(coder aDecoder: NSCoder) {  super.init(coder: aDecoder)  scrollDirection = UICollectionViewScrollDirection.Horizontal //1  itemSize = CGSizeMake(PageWidth, PageHeight) //2  minimumInteritemSpacing = 10 //3}

上述代码作用如下:

  1. 设置Collectioin View的滚动方向为水平方向。
  2. 设置单元格的大小为PageWidth和PageHeight,即362x568。
  3. 设置两个单元格间距10。

然后,在init(coder:)方法中加入代码:

override func prepareLayout() {  super.prepareLayout()  //The rate at which we scroll the collection view.  //1  collectionView?.decelerationRate = UIScrollViewDecelerationRateFast  //2  collectionView?.contentInset = UIEdgeInsets(    top: 0,    left: collectionView!.bounds.width / 2 - PageWidth / 2,    bottom: 0,    right: collectionView!.bounds.width / 2 - PageWidth / 2  )}

prepareLayout()方法允许我们在每个单元格的布局信息生效之前可以进行一些计算。

对应注释中的编号,以上代码分别说明如下:

  1. 设置当用户手指离开后,Collection
    View停止滚动的速度。默认的设置为UIScrollViewDecelerationRateFast,这是一个较快的速度。你可以尝试着设置为Normal 和 Fast,看看二者之间有什么区别。
  2. 设置Collection View的contentInset,以使第一本书的封面位于Collection View的中心。

现在我们需要处理每一个单元格的布局信息。
在prepareLayout()方法下面,加入以下代码:

override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {  //1  var array = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes]  //2  for attributes in array {    //3    var frame = attributes.frame    //4    var distance = abs(collectionView!.contentOffset.x + collectionView!.contentInset.left - frame.origin.x)    //5    var scale = 0.7 * min(max(1 - distance / (collectionView!.bounds.width), 0.75), 1)    //6    attributes.transform = CGAffineTransformMakeScale(scale, scale)  }  return array}

layoutAttributesForElementsInRect(_:) 方法返回一个UICollectionViewLayoutAttributes对象数组,其中包含了每一个单元格的布局属性。以上代码稍作说明如下:

  1. 调用父类的layoutAttributesForElementsInRect方法,已获得默认的单元格布局属性。
  2. 遍历数组中的每个单元格布局属性。
  3. 从单元格布局属性中读取frame。
  4. 计算两本书的封面之间的间距——即两个单元格之间的间距——以及屏幕的中心点。
  5. 以0.75~1之间的比率缩放封面,具体的比率取决于前面计算出来的间距。然后为了美观,将所有的封面都缩放70%。
  6. 最后,应用仿射变换。

接下来,在layoutAttributesForElementsInRect(_:)方法后增加如下代码:

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {  return true}

返回true表示每当Collection View的bounds发生改变时都强制重新计算布局属性。Collection View在滚动时会改变它的bounds,因此我们需要重新计算单元格的布局属性。

编译运行程序,我们将看到位于中央的封面明显比其他封面要大上一圈:

拖动Colleciton View,查看每本书放大、缩小。但仍然有一点稍显不足,为什么不让书本能够卡到固定的位置呢?
接下来我们介绍的这个方法就是干这个的。

对齐书本

targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:)方法用于计算每本书应该在对齐到哪个位置,它返回一个偏移位置,可用于设置Collection View的contentOffset。如果你不覆盖这个方法,它会返回一个默认的值。

在shouldInvalidateLayoutForBoundsChange(_:)方法后添加如下代码:

override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {  // Snap cells to centre  //1  var newOffset = CGPoint()  //2  var layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout  //3  var width = layout.itemSize.width + layout.minimumLineSpacing  //4  var offset = proposedContentOffset.x + collectionView!.contentInset.left  //5  if velocity.x > 0 {    //ceil returns next biggest number    offset = width * ceil(offset / width)  } else if velocity.x == 0 { //6    //rounds the argument    offset = width * round(offset / width)  } else if velocity.x < 0 { //7    //removes decimal part of argument    offset = width * floor(offset / width)  }  //8  newOffset.x = offset - collectionView!.contentInset.left  newOffset.y = proposedContentOffset.y //y will always be the same...  return newOffset}

这段代码计算当用户手指离开屏幕时,封面应该位于哪个偏移位置:

  1. 声明一个CGPoint。
  2. 获得Collection View的当前布局。
  3. 获得单元格的总宽度。
  4. 计算相对于屏幕中央的currentOffset。
  5. 如果velocity.x>0,表明用户向右滚动,用offset除以width,得到书的索引,并滚动到相应的位置。
  6. 如果velocity.x=0,表明用户是无意识的滚动,原来的选择不会发生改变。
  7. 如果velocity.x<0,表明用户向左滚动。
  8. 修改newOffset.x,然后返回newOffset。这样就保证书本总是对齐到屏幕的中央。

编译运行程序;再次滚动封面,你会注意到滚动动作将变得更整齐了。

要完成这个布局,我们还需要使用一种机制,以限制用户只能点击位于中央的封面。目前,不管哪个位置的封面都是可点击的。

打开BooksViewController.swift,在注释”//MARK:Helpers”下面加入以下代码:

func selectedCell() -> BookCoverCell? {  if let indexPath = collectionView?.indexPathForItemAtPoint(CGPointMake(collectionView!.contentOffset.x + collectionView!.bounds.width / 2, collectionView!.bounds.height / 2)) {    if let cell = collectionView?.cellForItemAtIndexPath(indexPath) as? BookCoverCell {      return cell    }  }  return nil}

selectedCell()方法返回位于中央的那个单元格。
替换openBook(_:)方法的代码如下:

func openBook() {  let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController  vc.book = selectedCell()?.book  // UICollectionView loads it's cells on a background thread, so make sure it's loaded before passing it to the animation handler  dispatch_async(dispatch_get_main_queue(), { () -> Void in    self.navigationController?.pushViewController(vc, animated: true)    return  })}

这里,直接调用新的selectedCell方法,并用它的book属性代替原来的book参数。
然后,将collectionView(_:didSelectItemAtIndexPath:)方法替换为:

override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {  openBook()}

这里,我们简单地删除了原来的打开某个索引处的图书的代码,而直接打开了当前位于屏幕中央的图书。
编译运行程序,我们将看到每次打开的图书总是位于屏幕中央的那本。

BooksLayout的工作就到这里了。后面我们将使这本电子书显得更真实,能够让用户”翻动“书里的每一页!

1 0