- 原文链接 : 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 = UIScrollViewDecelerationRateFast
numberOfItems = collectionView!.numberOfItemsInSection(0)
collectionView?.pagingEnabled = true
}
这段代码和我们在BooksLayout中所写的差不多,仅有以下几处差别:
仍然在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]? {
//1
var array: [UICollectionViewLayoutAttributes] = []
//2
for i in 0 ... max(0, numberOfItems - 1) {
//3
var indexPath = NSIndexPath(forItem: i, inSection: 0)
//4
var attributes = layoutAttributesForItemAtIndexPath(indexPath)
if attributes != nil {
//5
array += [attributes]
}
}
//6
return array
}
不同于在BooksLayout中的将所有计算布局属性的代码放在这个方法里面,我们这次将这个任务放到layoutAttributesForItemAtIndexPath(_:)中进行,因为在图书的实现中,所有单元格都是同时可见的。
以上代码解释如下:
在我们开始实现layoutAttributesForItemAtIndexPath(_:)方法之前,花几分钟好好思考一下布局的问题,它是怎样实现的,如果能够写几个助手方法将会让我们的代码更漂亮和模块化。:]
上图演示了翻页时以书籍为轴旋转的过程。图中书页的”打开度“用-1到1来表示。为什么?你可以想象一下放在桌子上的一本书,书脊所在的位置代表0.0。当你从左向右翻动书页时,书页张开的程度从-1(最左)到1(最右)。因此,我们可以用下列数字表示“翻页”的过程:
注意,因为角度是按照反时针方向增加的,因此角度的符号和对应的打开度是相反的。
首先,在layoutAttributesForElementsInRect(_:)方法后加入助手方法:
//MARK: - Attribute Logic Helpers
func getFrame(collectionView: UICollectionView) -> CGRect {
var frame = CGRect()
frame.origin.x = (collectionView.bounds.width / 2) - (PageWidth / 2) + collectionView.contentOffset.x
frame.origin.y = (collectionViewContentSize().height - PageHeight) / 2
frame.size.width = PageWidth
frame.size.height = PageHeight
return frame
}
对于每一页,我们都可以计算出相对于Collection View中心的frame。getFrame(_:)方法会将每一页的一边对齐到书脊。唯一会变的是Collectoin View的contentOffset在x方向上的改变。
然后,在getFrame(_:)方法后添加如下方法:
func getRatio(collectionView: UICollectionView, indexPath: NSIndexPath) -> CGFloat {
//1
let page = CGFloat(indexPath.item - indexPath.item % 2) * 0.5
//2
var ratio: CGFloat = -0.5 + page - (collectionView.contentOffset.x / collectionView.bounds.width)
//3
if 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
}
上面的方法计算书页翻开的程度。对每一段有注释的代码分别说明如下:
一旦我们计算出书页的打开度,我们就可以将之转变为旋转的角度。
在getRation(_:indexPath:)方法后面加入代码:
func getAngle(indexPath: NSIndexPath, ratio: CGFloat) -> CGFloat {
// Set rotation
var angle: CGFloat = 0
//1
if indexPath.item % 2 == 0 {
// The book's spine is on the left of the page
angle = (1-ratio) * CGFloat(-M_PI_2)
} else {
//2
// The book's spine is on the right of the page
angle = (1 + ratio) * CGFloat(M_PI_2)
}
//3
// Make sure the odd and even page don't have the exact same angle
angle += CGFloat(indexPath.row % 2) / 1000
//4
return angle
}
这个方法中有大量计算,我们一点点拆开来看:
得到旋转角度之后,我们可以操纵书页使其旋转。增加如下方法:
func makePerspectiveTransform() -> CATransform3D {
var transform = CATransform3DIdentity
transform.m34 = 1.0 / -2000
return 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! {
//1
var layoutAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
//2
var frame = getFrame(collectionView!)
layoutAttributes.frame = frame
//3
var ratio = getRatio(collectionView!, indexPath: indexPath)
//4
if ratio > 0 && indexPath.item % 2 == 1
|| ratio < 0 && indexPath.item % 2 == 0 {
// Make sure the cover is always visible
if indexPath.row != 0 {
return nil
}
}
//5
var rotation = getRotation(indexPath, ratio: min(max(ratio, -1), 1))
layoutAttributes.transform3D = rotation
//6
if indexPath.row == 0 {
layoutAttributes.zIndex = Int.max
}
return layoutAttributes
}
在Collection View的每个单元格上,都会调用这个方法。这个方法做了如下工作:
编译,运行。打开书,翻动每一页……呃?什么情况?
书被错误地从中间装订了,而不是从书的侧边装订。
如图中所示,每个书页的锚点默认是x轴和y轴的0.5倍处。现在你知道怎么做了吗?
很显然,我们需要修改书页的锚点为它的侧边缘。如果这个书页是位于书的右边,则它的锚点应该是(0,0.5)。如果书页是位于书的左边,测锚点应该是(1,0.5)。
打开BookePageCell.swift,添加如下代码:
override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
super.applyLayoutAttributes(layoutAttributes)
//1
if layoutAttributes.indexPath.item % 2 == 0 {
//2
layer.anchorPoint = CGPointMake(0, 0.5)
isRightPage = true
} else { //3
//4
layer.anchorPoint = CGPointMake(1, 0.5)
isRightPage = false
}
//5
self.updateShadowLayer()
}
我们重写了applyLayoutAttributes(_:)方法,这个方法用于将BookLoayout创建的布局属性应用到第一个。
上述代码非常简单:
编译,运行。翻动书页,这次的效果已经好了许多:
本教程第一部分就到此为止了。你是不是觉得自己干了一件了不起的事情呢——效果做得很棒,不是吗?
第一部分的完整代码在此处下载:http://cdn1.raywenderlich.com/wp-content/uploads/2015/05/Part-1-Paper-Completed.zip
我们从一个默认的Collection View布局开始,学习了如何定制自己的layout,并用它取得了令人叫绝的效果!用户在用这个App时,会觉得自己真的是在翻一本书。其实,将一个普通的电子阅读App变成一个让用户体验更加真实的带翻页效果的App,只需要做很小的改动。
当然,我们还没有完成这个App。在本教程的第二部分,我们将使App变得更加完善和生动,我们将在打开书和关上书的一瞬间加入自定义动画。
在开发App过程中,你也可能曾经有过一些关于布局的“疯狂”想法。如果你对本文有任何问题、意见或想法,请加入到下面的讨论中来!