如何實現(xiàn)iOS圖書動畫:第1部分
原文鏈接 : How to Create an iOS Book Open Animation: Part 1 原文作者 : Vincent Ngo 譯文出自 : 開發(fā)技術(shù)前線 www.devtf.cn 譯者 : kmyhy本教程分為2個部分,教你開發(fā)一個漂亮的iOS圖書打開和翻頁動畫,就像你在Paper 53中所見到的一樣:
在第1部分,你將學(xué)習(xí)到如何定制化Collection View Layout,并通過使用深度和陰影使App看起來更真實,
如何實現(xiàn)iOS圖書動畫:第1部分(上)
。在第2部分,你將學(xué)習(xí)如何以一種合理的方法在兩個不同的控制器之間創(chuàng)建自定義的過渡特效,以及利用手勢在兩個視圖間創(chuàng)建自然的、直觀的過渡效果。
本教程適用于中級-高級的開發(fā)者;你將使用自定義過渡動畫和自定義Collection View Layout。如果你從來沒有用過Colleciton View,請先參考其他iOS教程。
注意:感謝Attila Hegdüs創(chuàng)建了本教程中的示例項目。
開始
從此處下載本教程的開始項目;解開zip壓縮包,用Xcode打開Paper.xcodeproj。
編譯項目,在模擬器中運行App;你將看到如下畫面:
這個App的功能已經(jīng)很完善了,你可以在你的書庫中滾動,查看圖書,選中某本圖書進(jìn)行瀏覽。但當(dāng)你讀一本書的時候,為什么它的書頁都是并排放置的?通過一些UICollectionView的知識,你可以讓這些書頁看起來更好一些!
項目結(jié)構(gòu)
Here’s a quick rundown of the most important bits of the starter project:
關(guān)于這個開始項目,有幾個重要的地方需要解釋:
Data Models文件夾包含3個文件:
Books.plist 中包含了幾本用于演示的圖書信息。每本圖書包含一張封面圖片,以及一個表示每一頁的內(nèi)容的圖片的數(shù)組。 BookStore.swift實現(xiàn)了單例,在整個App聲明周期中只能創(chuàng)建一次對象。BookStore的職責(zé)是從Books.plist中加載數(shù)據(jù)并創(chuàng)建Book類實例。 Book.swift用于存放圖書相關(guān)信息的類,比如圖書的封面,每一頁的圖片,以及頁號。Books文件夾包含了兩個文件:
BooksViewController.swift是一個UICollectionViewController子類。負(fù)責(zé)以水平方式顯式圖書列表。 BookCoverCell.swift負(fù)責(zé)顯示圖書的封面,這個類被BooksViewController類所引用。在Book文件夾中則包括:
BookViewController.swift也是UICollectionViewController的子類。當(dāng)用戶在BooksViewController中選定的一本書后,它負(fù)責(zé)顯示圖書中的書頁。 BookPageCell.swift被BookViewController用于顯示圖書中的書頁。在最后一個文件夾Helper中包含了:
UIImage+Helpers.swift是UIImage的擴(kuò)展。該擴(kuò)展包含了兩個實用方法,一個用于讓圖片呈圓角顯示,一個用于將圖片縮放到指定大小。這就是整個開始項目的大致介紹——接下來該是我們寫點代碼的時候了!
定制化圖書界面
首先我們需要在BooksViewController中覆蓋Collection View的默認(rèn)布局方式。但當(dāng)前的布局是在屏幕上顯示3張圖書封面的大圖。為了美觀,我們將這些圖片縮減到一定大小,如下圖所示:
當(dāng)我們滑動圖片,移動到屏幕中心的圖片將被放大,以表示該圖書為選中狀態(tài)。如果繼續(xù)滑動,該圖書的封面又會縮小到一邊,表示我們放棄選擇該圖書。
在AppBooks文件夾下新建一個文件夾組:Layout。在Layout上點擊右鍵,選擇New File…,然后選擇iOSSourceCocoa Touch Class模板,并點擊Next。類名命名為BooksLayout,繼承UICollectionViewFlowLayout類,語言設(shè)置為Swift。
然后需要告訴BooksViewController中的Collection View,適用我們新建的BooksLayout。
打開Main.storyboard,展開BooksViewController對象,然后選擇Collection View。在屬性面板中,設(shè)置Layout 屬性為 Custom,設(shè)置Class屬性為BooksLayout,如下圖所示:
打開BooksLayout.swift,在BooksLayout類聲明之上加入以下代碼:
<code class="hljs" cs="">private let PageWidth: CGFloat = 362private let PageHeight: CGFloat = 568</code>
這個兩個常量將用于設(shè)置單元格的的大小。
現(xiàn)在,在類定義內(nèi)部定義如下初始化方法:
<code class="hljs" java="">required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) scrollDirection = UICollectionViewScrollDirection.Horizontal //1 itemSize = CGSizeMake(PageWidth, PageHeight) //2 minimumInteritemSpacing = 10 //3}</code>
上述代碼作用如下:
設(shè)置Collectioin View的滾動方向為水平方向。 設(shè)置單元格的大小為PageWidth和PageHeight,即362x568。 設(shè)置兩個單元格間距10。然后,在init(coder:)方法中加入代碼:
<code avrasm="" class="hljs">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 )}</code>
prepareLayout()方法允許我們在每個單元格的布局信息生效之前可以進(jìn)行一些計算。
對應(yīng)注釋中的編號,以上代碼分別說明如下:
設(shè)置當(dāng)用戶手指離開后,CollectionView停止?jié)L動的速度。默認(rèn)的設(shè)置為UIScrollViewDecelerationRateFast,這是一個較快的速度。你可以嘗試著設(shè)置為Normal 和 Fast,看看二者之間有什么區(qū)別。 設(shè)置Collection View的contentInset,以使第一本書的封面位于Collection View的中心。
現(xiàn)在我們需要處理每一個單元格的布局信息。
在prepareLayout()方法下面,加入以下代碼:
<code class="hljs" php="">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}</code>
layoutAttributesForElementsInRect(_:) 方法返回一個UICollectionViewLayoutAttributes對象數(shù)組,其中包含了每一個單元格的布局屬性,
電腦資料
《如何實現(xiàn)iOS圖書動畫:第1部分(上)》(http://m.clearvueentertainment.com)。以上代碼稍作說明如下:調(diào)用父類的layoutAttributesForElementsInRect方法,已獲得默認(rèn)的單元格布局屬性。 遍歷數(shù)組中的每個單元格布局屬性。 從單元格布局屬性中讀取frame。 計算兩本書的封面之間的間距——即兩個單元格之間的間距——以及屏幕的中心點。 以0.75~1之間的比率縮放封面,具體的比率取決于前面計算出來的間距。然后為了美觀,將所有的封面都縮放70%。 最后,應(yīng)用仿射變換。接下來,在layoutAttributesForElementsInRect(_:)方法后增加如下代碼:
<code class="hljs" coffeescript="">override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { return true}</code>
返回true表示每當(dāng)Collection View的bounds發(fā)生改變時都強(qiáng)制重新計算布局屬性。Collection View在滾動時會改變它的bounds,因此我們需要重新計算單元格的布局屬性。
編譯運行程序,我們將看到位于中央的封面明顯比其他封面要大上一圈:
拖動Colleciton View,查看每本書放大、縮小。但仍然有一點稍顯不足,為什么不讓書本能夠卡到固定的位置呢?
接下來我們介紹的這個方法就是干這個的。
對齊書本
targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:)方法用于計算每本書應(yīng)該在對齊到哪個位置,它返回一個偏移位置,可用于設(shè)置Collection View的contentOffset。如果你不覆蓋這個方法,它會返回一個默認(rèn)的值。
在shouldInvalidateLayoutForBoundsChange(_:)方法后添加如下代碼:
<code class="hljs" livecodeserver="">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 ffset = proposedContentOffset.x + collectionView!.contentInset.left //5 if velocity.x > 0 { //ceil returns next biggest number ffset = width * ceil(offset / width) } else if velocity.x == 0 { //6 //rounds the argument ffset = width * round(offset / width) } else if velocity.x < 0 { //7 //removes decimal part of argument ffset = width * floor(offset / width) } //8 newOffset.x = offset - collectionView!.contentInset.left newOffset.y = proposedContentOffset.y //y will always be the same... return newOffset}</code>
這段代碼計算當(dāng)用戶手指離開屏幕時,封面應(yīng)該位于哪個偏移位置:
聲明一個CGPoint。 獲得Collection View的當(dāng)前布局。 獲得單元格的總寬度。 計算相對于屏幕中央的currentOffset。 如果velocity.x>0,表明用戶向右滾動,用offset除以width,得到書的索引,并滾動到相應(yīng)的位置。 如果velocity.x=0,表明用戶是無意識的滾動,原來的選擇不會發(fā)生改變。 如果velocity.x<0,表明用戶向左滾動。 修改newOffset.x,然后返回newOffset。這樣就保證書本總是對齊到屏幕的中央。編譯運行程序;再次滾動封面,你會注意到滾動動作將變得更整齊了。
要完成這個布局,我們還需要使用一種機(jī)制,以限制用戶只能點擊位于中央的封面。目前,不管哪個位置的封面都是可點擊的。
打開BooksViewController.swift,在注釋”//MARK:Helpers”下面加入以下代碼:
<code class="hljs" coffeescript="">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}</code>
selectedCell()方法返回位于中央的那個單元格。
替換openBook(_:)方法的代碼如下:
<code class="hljs" coffeescript="">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 })}</code>
這里,直接調(diào)用新的selectedCell方法,并用它的book屬性代替原來的book參數(shù)。
然后,將collectionView(_:didSelectItemAtIndexPath:)方法替換為:
<code class="hljs" scss="">override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { openBook()}</code>
這里,我們簡單地刪除了原來的打開某個索引處的圖書的代碼,而直接打開了當(dāng)前位于屏幕中央的圖書。
編譯運行程序,我們將看到每次打開的圖書總是位于屏幕中央的那本。