如何实现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 = 362
private let PageHeight: CGFloat = 568

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

required init(coder aDecoder: NSCoder) {super.init(coder: aDecoder)scrollDirection = UICollectionViewScrollDirection.Horizontal //1itemSize = CGSizeMake(PageWidth, PageHeight) //2minimumInteritemSpacing = 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.//1collectionView?.decelerationRate = UIScrollViewDecelerationRateFast//2collectionView?.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]? {//1var array = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes]//2for attributes in array {//3var frame = attributes.frame//4var distance = abs(collectionView!.contentOffset.x + collectionView!.contentInset.left - frame.origin.x)//5var scale = 0.7 * min(max(1 - distance / (collectionView!.bounds.width), 0.75), 1)//6attributes.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 //1var newOffset = CGPoint() //2var layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout //3var width = layout.itemSize.width + layout.minimumLineSpacing //4var offset = proposedContentOffset.x + collectionView!.contentInset.left //5if velocity.x > 0 { //ceil returns next biggest numberoffset = width * ceil(offset / width)} else if velocity.x == 0 { //6 //rounds the argumentoffset = width * round(offset / width)} else if velocity.x < 0 { //7 //removes decimal part of argumentoffset = width * floor(offset / width)} //8newOffset.x = offset - collectionView!.contentInset.leftnewOffset.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! BookViewControllervc.book = selectedCell()?.book// UICollectionView loads it's cells on a background thread, so make sure it's loaded before passing it to the animation handlerdispatch_async(dispatch_get_main_queue(), { () -> Void inself.navigationController?.pushViewController(vc, animated: true)return})
}

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

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

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

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

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

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

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

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

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

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

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

  4. iOS常用动画 类封装

    //这是一个很好的动画封装类 很容易明白很详细 和大家分享 // CoreAnimationEffect.h // CoreAnimationEffect // // Created by Vince ...

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

    注意:我为过渡动画写了两篇文章: 第一篇:[iOS]过渡动画之简单模仿系统,主要分析系统简单的动画实现原理,以及讲解坐标系.绝对坐标系.相对坐标系,坐标系转换等知识,为第二篇储备理论基础.最后实现 M ...

  6. iOS核心动画学习整理

    最近利用业余时间终于把iOS核心动画高级技巧(https://zsisme.gitbooks.io/ios-/content/chapter1/the-layer-tree.html)看完,对应其中一 ...

  7. 转-- iOS 30多个iOS常用动画,带详细注释

    // // CoreAnimationEffect.h // CoreAnimationEffect // // Created by VincentXue on 13-1-19. // Copyri ...

  8. iOS 核心动画 Core Animation浅谈

    代码地址如下: http://www.demodashi.com/demo/11603.html 前记 关于实现一个iOS动画,如果简单的,我们可以直接调用UIView的代码块来实现,虽然使用UIVi ...

  9. iOS核心动画高级技术(九) 图层时间

    The biggest difference between time and space is that you can't reuse time. 时间和空间最大的区别在于,时间不能被复用 -- ...

最新文章

  1. 利用Powershell查询AD中账号属性
  2. 渗透测试小马(一句话)篇
  3. 创业者在创业时经常会问到的一个问题
  4. java ant 详解
  5. df命令--Linux命令应用大词典729个命令解读
  6. 更换pip源,解决pip install安装包慢的问题
  7. python自动登录百度_Python实现自动登录百度空间的方法
  8. 关于“undefined reference to”错误
  9. 移动端 H5图片裁剪插件,内置简单手势操作
  10. 《终极算法》阅读笔记与摘要(1)-序和第1-2章
  11. 怎么把vivo强行刷入鸿蒙系统,vivo手机如何强制刷机
  12. pip 安装GPU版本pytorch 与cuda下载
  13. 清华上交等发表Nature子刊!分片线性神经网络最新综述!
  14. java程序一维数组能被5整除,JAVA鏈熸湯璇曢闆?鍚瓟妗? - 鐧惧害鏂囧簱
  15. DirectX修复工具使用
  16. StretchSense 打造了一款真正舒适的手部动作捕捉手套
  17. 维特比译码算法(Viterbi decoding algorithm)
  18. 京东T7团队技术4面:线程池+索引+Spring +分布式锁+Mysql+项目等
  19. 小罗与卡卡辞别世界杯
  20. 计算机考试 虚拟机,虚拟机在全国计算机等级考试机试中的应用.doc

热门文章

  1. 鸟哥私房菜学习(二)Linux是什么与如何学习
  2. 【在线实习】推推项目课程介绍—小说更新就通知
  3. “小说列表模块”前端代码官方评审「在线实习·推推」
  4. freeswitch php监听,程序员罗杰-freeswitch对接asterisk压测
  5. 我账号的私信总是被删,请勿私信联系
  6. 运维工程师的30个头衔
  7. Fabric 账本数据块结构解析(二):如何解析账本中的配置块数据
  8. 打印服务器ip地址怎么修改,如何更改打印服务器的IP地址?
  9. 人类轨迹记录的另类诠释——《智慧学九论》
  10. encoder- Linux从入门到精通【下】