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