你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> 如何實現iOS圖書動畫:第1部分(下)

如何實現iOS圖書動畫:第1部分(下)

編輯:IOS開發綜合
原文鏈接 : How to Create an iOS Book Open Animation: Part 1 原文作者 : Vincent Ngo 譯文出自 : 開發技術前線 www.devtf.cn 譯者 : kmyhy

翻頁布局

最終實現的效果如下:

這看起來就像是一本真正的書! :]

在Book文件夾下新建一個Layout文件夾。在Layout文件夾上右鍵,選擇New File…,然後適用iOSSourceCocoa 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中所寫的差不多,僅有以下幾處差別:

將減速速度設置為UIScrollViewDecelerationRateFast,以加快Scroll View滾動速度變慢的節奏。 記住本書的頁數。 啟用分頁,這樣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]? {
  //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(_:)中進行,因為在圖書的實現中,所有單元格都是同時可見的。

以上代碼解釋如下:

聲明一個數組,用於保存所有單元格的布局屬性。 遍歷所有的書頁。 對於CollecitonView中的每個單元格,都創建一個NSIndexPath。 通過每個NSIndexPath來獲得單元格的布局屬性,在後面,我們會覆蓋layoutAttributesForItemAtIndexPath(_:)方法。 把每個單元格布局屬性添加到數組。 返回數組。

處理頁面的幾何計算

在我們開始實現layoutAttributesForItemAtIndexPath(_:)方法之前,花幾分鐘好好思考一下布局的問題,它是怎樣實現的,如果能夠寫幾個助手方法將會讓我們的代碼更漂亮和模塊化。:]

上圖演示了翻頁時以書籍為軸旋轉的過程。圖中書頁的”打開度“用-1到1來表示。為什麼?你可以想象一下放在桌子上的一本書,書脊所在的位置代表0.0。當你從左向右翻動書頁時,書頁張開的程度從-1(最左)到1(最右)。因此,我們可以用下列數字表示“翻頁”的過程:

0.0表示一個書頁翻成90度,與桌面成直角。 +/-0.5表示書頁翻至於桌面成45度角。 +/-1.0表示書頁翻至與桌面平行。

注意,因為角度是按照反時針方向增加的,因此角度的符號和對應的打開度是相反的。

首先,在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
}

上面的方法計算書頁翻開的程度。對每一段有注釋的代碼分別說明如下:

算出書頁的頁碼——記住,書是雙面的。 除以2就是你真正在翻讀的那一頁。 算出書頁的打開度。注意,這個值被我們加了一個權重。 書頁的打開度必須限制在-0.5到0.5之間。另外乘以0.1的作用,是為位了在頁與頁之間增加一條細縫,以表示它們是上下疊放在一起的。

一旦我們計算出書頁的打開度,我們就可以將之轉變為旋轉的角度。
在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
}

這個方法中有大量計算,我們一點點拆開來看:

判斷該頁是否是偶數頁。如果是,則該頁將翻到書脊的右邊。翻到右邊的頁是反手翻轉,同時書脊右邊的頁其角度必然是負數。注意,我們將打開度定義為-0.5到0.5之間。 如果當前頁是奇數,則該頁將位於書脊左邊,當書頁被翻到左邊時,它的按正手翻轉,書脊左邊的頁其角度為正數。 每頁之間加一個小夾角,使它們彼此分離。 返回旋轉角度。

得到旋轉角度之後,我們可以操縱書頁使其旋轉。增加如下方法:

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的每個單元格上,都會調用這個方法。這個方法做了如下工作:

創建一個UICollectionViewLayoutAttributes對象layoutAttributes,供IndexPath所指的單元格使用。 調用我們先前定義的getFrame方法設置layoutAttributes的frame,確保單元格對齊於書脊。 調用先前定義的getRatio方法算出單元格的打開度。 判斷當前頁是否位於正確的打開度范圍之內。如果不,不顯示該單元格。為了優化(也是為了符合常理),除了正面向上的書頁,我們不應當顯示書頁的背面——書的封面例外,那個不管什麼時候都需要顯示。 應用旋轉動畫,使用前面算出的打開度。 判斷是否是第一頁,如果是,將它的zIndex放在其他頁的上面,否則有可能出現畫面閃爍的Bug。

編譯,運行。打開書,翻動每一頁……呃?什麼情況?

書被錯誤地從中間裝訂了,而不是從書的側邊裝訂。

如圖中所示,每個書頁的錨點默認是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創建的布局屬性應用到第一個。
上述代碼非常簡單:

檢查當前單元格是否是偶數,也就是說書脊是否位於書頁的左邊。 如果是,將書頁的錨點設置為單元格的左邊緣,同時isRightPage設置為true。isRightPage變量可用於決定書頁的圓角樣式應當在那一邊應用。 如果是奇數頁,書脊應當位於書頁的右邊。 這只書頁的錨點為單元格的右邊緣,isRightPage設為false。 最後,設置當前書頁的陰影層。

編譯,運行。翻動書頁,這次的效果已經好了許多:

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

 

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved