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

翻页布局

最终实现的效果如下:

这看起来就像是一本真正的书! :]

在Book文件夹下新建一个Layout文件夹。在Layout文件夹上右键,选择New File…,然后适用iOS\Source\Cocoa Touch Class模板,然后点Next。类名命名为BookLayout,继承于UICollectionViewFlowLayout,语言选择Swift。

同前面一样,图书所使用的Collection View需要适用新的布局。打开Main.storyboard,然后选择Book View Controller场景,展开并选中其中的Collection View,然后设置Layout属性为Custom。

然后,将Layout属性下的 Class属性设置为BookLayout:

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

private let PageWidth: CGFloat = 362
private let PageHeight: CGFloat = 568
private var numberOfItems = 0

这几个常量将用于设置单元格的大小,以及记录整本书的页数。
接着,在类声明内部加入代码:

override func prepareLayout() {super.prepareLayout()collectionView?.decelerationRate = UIScrollViewDecelerationRateFastnumberOfItems = collectionView!.numberOfItemsInSection(0)collectionView?.pagingEnabled = true
}

这段代码和我们在BooksLayout中所写的差不多,仅有以下几处差别:

  1. 将减速速度设置为UIScrollViewDecelerationRateFast,以加快Scroll View滚动速度变慢的节奏。
  2. 记住本书的页数。
  3. 启用分页,这样Scroll View滚动时将以其宽度的固定倍数滚动(而不是持续滚动)。

仍然在BookLayout.swift中,加入以下代码:

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

跟前面一样,返回true,以表示用户每次滚动都会重新计算布局。

然后,覆盖collectionViewContentSize() ,以指定Collection View的contentSize:

override func collectionViewContentSize() -> CGSize {return CGSizeMake((CGFloat(numberOfItems / 2)) * collectionView!.bounds.width, collectionView!.bounds.height)
}

这个方法返回了内容区域的整个大小。内容区域的高度总是不变的,但宽度是随着页数变化的——即书的页数除以2倍,再乘以屏幕宽度。除以2是因为书页有两面,内容区域一次显示2页。

就如我们在BooksLayout中所做的一样,我们还需要覆盖layoutAttributesForElementsInRect(_:)方法,以便我们能够在单元格上增加翻页效果。

在collectionViewContentSize()方法后加入:

override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {//1var array: [UICollectionViewLayoutAttributes] = []//2for i in 0 ... max(0, numberOfItems - 1) {//3var indexPath = NSIndexPath(forItem: i, inSection: 0)//4var attributes = layoutAttributesForItemAtIndexPath(indexPath)if attributes != nil {//5array += [attributes]}}//6return array
}

不同于在BooksLayout中的将所有计算布局属性的代码放在这个方法里面,我们这次将这个任务放到layoutAttributesForItemAtIndexPath(_:)中进行,因为在图书的实现中,所有单元格都是同时可见的。

以上代码解释如下:

  1. 声明一个数组,用于保存所有单元格的布局属性。
  2. 遍历所有的书页。
  3. 对于CollecitonView中的每个单元格,都创建一个NSIndexPath。
  4. 通过每个NSIndexPath来获得单元格的布局属性,在后面,我们会覆盖layoutAttributesForItemAtIndexPath(_:)方法。
  5. 把每个单元格布局属性添加到数组。
  6. 返回数组。

处理页面的几何计算

在我们开始实现layoutAttributesForItemAtIndexPath(_:)方法之前,花几分钟好好思考一下布局的问题,它是怎样实现的,如果能够写几个助手方法将会让我们的代码更漂亮和模块化。:]

上图演示了翻页时以书籍为轴旋转的过程。图中书页的”打开度“用-1到1来表示。为什么?你可以想象一下放在桌子上的一本书,书脊所在的位置代表0.0。当你从左向右翻动书页时,书页张开的程度从-1(最左)到1(最右)。因此,我们可以用下列数字表示“翻页”的过程:

  1. 0.0表示一个书页翻成90度,与桌面成直角。
  2. +/-0.5表示书页翻至于桌面成45度角。
  3. +/-1.0表示书页翻至与桌面平行。

注意,因为角度是按照反时针方向增加的,因此角度的符号和对应的打开度是相反的。

首先,在layoutAttributesForElementsInRect(_:)方法后加入助手方法:

//MARK: - Attribute Logic Helpersfunc getFrame(collectionView: UICollectionView) -> CGRect {var frame = CGRect()frame.origin.x = (collectionView.bounds.width / 2) - (PageWidth / 2) + collectionView.contentOffset.xframe.origin.y = (collectionViewContentSize().height - PageHeight) / 2frame.size.width = PageWidthframe.size.height = PageHeightreturn frame
}

对于每一页,我们都可以计算出相对于Collection View中心的frame。getFrame(_:)方法会将每一页的一边对齐到书脊。唯一会变的是Collectoin View的contentOffset在x方向上的改变。

然后,在getFrame(_:)方法后添加如下方法:

func getRatio(collectionView: UICollectionView, indexPath: NSIndexPath) -> CGFloat {//1let page = CGFloat(indexPath.item - indexPath.item % 2) * 0.5//2var ratio: CGFloat = -0.5 + page - (collectionView.contentOffset.x / collectionView.bounds.width)//3if ratio > 0.5 {ratio = 0.5 + 0.1 * (ratio - 0.5)} else if ratio < -0.5 {ratio = -0.5 + 0.1 * (ratio + 0.5)}return ratio
}

上面的方法计算书页翻开的程度。对每一段有注释的代码分别说明如下:

  1. 算出书页的页码——记住,书是双面的。 除以2就是你真正在翻读的那一页。
  2. 算出书页的打开度。注意,这个值被我们加了一个权重。
  3. 书页的打开度必须限制在-0.5到0.5之间。另外乘以0.1的作用,是为位了在页与页之间增加一条细缝,以表示它们是上下叠放在一起的。

一旦我们计算出书页的打开度,我们就可以将之转变为旋转的角度。
在getRation(_:indexPath:)方法后面加入代码:

func getAngle(indexPath: NSIndexPath, ratio: CGFloat) -> CGFloat {// Set rotationvar angle: CGFloat = 0//1if indexPath.item % 2 == 0 {// The book's spine is on the left of the pageangle = (1-ratio) * CGFloat(-M_PI_2)} else {//2// The book's spine is on the right of the pageangle = (1 + ratio) * CGFloat(M_PI_2)}//3// Make sure the odd and even page don't have the exact same angleangle += CGFloat(indexPath.row % 2) / 1000//4return angle
}

这个方法中有大量计算,我们一点点拆开来看:

  1. 判断该页是否是偶数页。如果是,则该页将翻到书脊的右边。翻到右边的页是反手翻转,同时书脊右边的页其角度必然是负数。注意,我们将打开度定义为-0.5到0.5之间。
  2. 如果当前页是奇数,则该页将位于书脊左边,当书页被翻到左边时,它的按正手翻转,书脊左边的页其角度为正数。
  3. 每页之间加一个小夹角,使它们彼此分离。
  4. 返回旋转角度。

得到旋转角度之后,我们可以操纵书页使其旋转。增加如下方法:

func makePerspectiveTransform() -> CATransform3D {var transform = CATransform3DIdentitytransform.m34 = 1.0 / -2000return transform
}

修改转换矩阵的m34属性,已达到一定的立体效果。
然后应用旋转动画。实现下面的方法:

func getRotation(indexPath: NSIndexPath, ratio: CGFloat) -> CATransform3D {var transform = makePerspectiveTransform()var angle = getAngle(indexPath, ratio: ratio)transform = CATransform3DRotate(transform, angle, 0, 1, 0)return transform
}

在这个方法中,我们用到了刚才创建的两个助手方法去计算旋转的角度,然后通过一个CATransform3D对象让书页在y轴上旋转。

所有的助手方法都实现了,我们最终需要配置每个单元格的属性。在layoutAttributesForElementsInRect(_:)方法后加入以下方法:

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {//1var layoutAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)//2var frame = getFrame(collectionView!)layoutAttributes.frame = frame//3var ratio = getRatio(collectionView!, indexPath: indexPath)//4if ratio > 0 && indexPath.item % 2 == 1|| ratio < 0 && indexPath.item % 2 == 0 {// Make sure the cover is always visibleif indexPath.row != 0 {return nil}} //5var rotation = getRotation(indexPath, ratio: min(max(ratio, -1), 1))layoutAttributes.transform3D = rotation//6if indexPath.row == 0 {layoutAttributes.zIndex = Int.max}return layoutAttributes
}

在Collection View的每个单元格上,都会调用这个方法。这个方法做了如下工作:

  1. 创建一个UICollectionViewLayoutAttributes对象layoutAttributes,供IndexPath所指的单元格使用。
  2. 调用我们先前定义的getFrame方法设置layoutAttributes的frame,确保单元格对齐于书脊。
  3. 调用先前定义的getRatio方法算出单元格的打开度。
  4. 判断当前页是否位于正确的打开度范围之内。如果不,不显示该单元格。为了优化(也是为了符合常理),除了正面向上的书页,我们不应当显示书页的背面——书的封面例外,那个不管什么时候都需要显示。
  5. 应用旋转动画,使用前面算出的打开度。
  6. 判断是否是第一页,如果是,将它的zIndex放在其他页的上面,否则有可能出现画面闪烁的Bug。

编译,运行。打开书,翻动每一页……呃?什么情况?

书被错误地从中间装订了,而不是从书的侧边装订。

如图中所示,每个书页的锚点默认是x轴和y轴的0.5倍处。现在你知道怎么做了吗?

很显然,我们需要修改书页的锚点为它的侧边缘。如果这个书页是位于书的右边,则它的锚点应该是(0,0.5)。如果书页是位于书的左边,测锚点应该是(1,0.5)。

打开BookePageCell.swift,添加如下代码:

override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {super.applyLayoutAttributes(layoutAttributes)//1if layoutAttributes.indexPath.item % 2 == 0 {//2layer.anchorPoint = CGPointMake(0, 0.5)isRightPage = true} else { //3//4layer.anchorPoint = CGPointMake(1, 0.5)isRightPage = false}//5self.updateShadowLayer()
}

我们重写了applyLayoutAttributes(_:)方法,这个方法用于将BookLoayout创建的布局属性应用到第一个。
上述代码非常简单:

  1. 检查当前单元格是否是偶数,也就是说书脊是否位于书页的左边。
  2. 如果是,将书页的锚点设置为单元格的左边缘,同时isRightPage设置为true。isRightPage变量可用于决定书页的圆角样式应当在那一边应用。
  3. 如果是奇数页,书脊应当位于书页的右边。
  4. 这只书页的锚点为单元格的右边缘,isRightPage设为false。
  5. 最后,设置当前书页的阴影层。

编译,运行。翻动书页,这次的效果已经好了许多:

本教程第一部分就到此为止了。你是不是觉得自己干了一件了不起的事情呢——效果做得很棒,不是吗?

接下来做什么

第一部分的完整代码在此处下载:http://cdn1.raywenderlich.com/wp-content/uploads/2015/05/Part-1-Paper-Completed.zip

我们从一个默认的Collection View布局开始,学习了如何定制自己的layout,并用它取得了令人叫绝的效果!用户在用这个App时,会觉得自己真的是在翻一本书。其实,将一个普通的电子阅读App变成一个让用户体验更加真实的带翻页效果的App,只需要做很小的改动。
当然,我们还没有完成这个App。在本教程的第二部分,我们将使App变得更加完善和生动,我们将在打开书和关上书的一瞬间加入自定义动画。
在开发App过程中,你也可能曾经有过一些关于布局的“疯狂”想法。如果你对本文有任何问题、意见或想法,请加入到下面的讨论中来!

如何实现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部分(上)

    如何实现iOS图书动画:第1部分 原文链接 : How to Create an iOS Book Open Animation: Part 1 原文作者 : Vincent Ngo 译文出自 : 开 ...

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

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

  5. iOS核心动画学习整理

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

  6. iOS 核心动画 Core Animation浅谈

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

  7. ios uiview动画_iOS UIView动画

    ios uiview动画 In this tutorial, we'll be animating our UI Views in various ways in the iOS Applicatio ...

  8. iOS核心动画详解swift版----基础动画

    2019独角兽企业重金招聘Python工程师标准>>> iOS核心动画详解swift版---基础动画 创建工程,添加2个ViewController,通过rootViewContro ...

  9. ae制h5文字动画_大杀器Bodymovin和Lottie:把AE动画转换成HTML5/Android/iOS原生动画

    前段时间听部门老大说,Airbnb出了个移动端的动画库Lottie,可以和一个名叫Bodymovin的AE插件结合起来,把在AE上做好的动画导出为json文件,然后以Android/iOS原生动画的形 ...

最新文章

  1. 借助队列解决Josephus问题
  2. 符号说明表怎么做_教会你的孩子正确使用标点符号
  3. java 同一个package import_【编程基础】Java 中的Package和Import
  4. Linux中printk和strace命令调试的一些技巧
  5. 解决Tomcat8及Tomcat7下http的post、get请求中参数中文乱码问题
  6. c++中的继承--2(继承中的析构函数和构造函数,继承中同名成员,继承中静态成员)
  7. python定义类的程序_python扫码签到程序python中如何定义类
  8. 记一次微信数据库解密过程
  9. java collection 常用类_分析Collection常用的实现类
  10. 51单片机C语言编程100例pdf,51单片机C语言编程100例.doc
  11. 《管理学》第十周阶段性回顾
  12. 为自己的APP搭建个简易后台
  13. ERROR 1366 (HY000): Incorrect string value: ‘\xE8\xB5\xB5\xE9\x9B\xB7‘ for column ‘s_name‘ at row 1
  14. 王都归来,山寨手机分抢市场
  15. 大唐杯比赛辅导,国一选手
  16. stata的固定效应,控制时间和个体的语句
  17. 无法访问计算机请检查名称的拼写,win10系统访问共享文件夹提示“请检查名称的拼写”的修复方案...
  18. ubuntu如何配置软件更新源和更新镜像
  19. Codeforces Round #257 (Div. 2)
  20. 质子交换膜燃料电池建模与控制研究

热门文章

  1. TAF(Total Application Framework) 基础通信协议 Tars协议
  2. 一号位是一种心态,而不是职级
  3. PSP dev lesson 05
  4. [导入]如何学习英语(英语学习中最重要的五点)
  5. word将参考文献引用链接到内容——交叉引用
  6. 硬盘变成RAW格式无法读取的解决办法
  7. 中国粘胶纤维市场消费量调研及投资商机研究报告2022-2028年
  8. ChatGPT+Midjourney,带你领略古诗词的魅力
  9. 陈欧侃:来自开源,反馈开源 —— “一铭杯”专访
  10. 李嘉诚的“自负指数”与盖茨的“自私基因”看成功需要什么?