教你用CollectionView做一個炫酷的旋轉輪

原文鏈接: UICollectionView Custom Layout Tutorial: A Spinning Wheel

本文翻譯有部分改動,使用OC編寫,原文使用的是Swift,如有需要,可以去原文下載Swift Demo,文章最后會提供OC的Demo。

開始

首先,去下載一個初始項目,原文是用的xib做了一個collectionView,我直接用代碼編寫了,最后得到的效果是這樣:


環(huán)形布局

在項目里創(chuàng)建一個CircularCollectionViewLayout,像這樣



因為這是UICollectionViewLayout的子類,而不是UICollectionViewFlowLayout的子類,所以你需要處理所有l(wèi)ayout的相關操作。

在CircularCollectionViewLayout里,創(chuàng)建itemSize和radius屬性

let itemSize = CGSize(width: 133, height: 173)

var radius: CGFloat = 500 {
  didSet {
    invalidateLayout()
  }
}

invalidateLayout的解釋是:Call -invalidateLayout to indicate that the collection view needs to requery the layout information. 這里將該方法放在didset里,當radius屬性改變時,你重新重新計算所有的屬性進行布局。
在radius聲明下面,定義一個anglePerItem

var anglePerItem: CGFloat {
  return atan(itemSize.width / radius)
}

這個屬性可以任意變化,不過這個方法可以保證使其不會間隔太遠。
接下來,重寫collectionViewContentSize方法來確定你的collection view的內(nèi)容大小

override func collectionViewContentSize() -> CGSize {
  return CGSize(width: CGFloat(collectionView!.numberOfItemsInSection(0)) * itemSize.width, height: CGRectGetHeight(collectionView!.bounds))
}

contentSize的高度應該和你的collectionView的高度一樣,而寬度應該為 itemSize.width * numberOfItems。
現(xiàn)在,你還需要去把你的CollectionView的layout設置為CircularCollectionViewLayout。

自定義Layout Attributes

你需要自定義一個layout attributes繼承自UICollectionViewLayoutAttributes來存儲angular position 和 anchorPoint屬性。
在CircularCollectionViewLayout文件里,該類的定義上面,添加一下代碼

class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
  // 1
  var anchorPoint = CGPoint(x: 0.5, y: 0.5)
  var angle: CGFloat = 0 {
    // 2 
    didSet {
      zIndex = Int(angle * 1000000)
      transform = CGAffineTransformMakeRotation(angle)
    }
  }
  // 3
  override func copyWithZone(zone: NSZone) -> AnyObject {
    let copiedAttributes: CircularCollectionViewLayoutAttributes = 
        super.copyWithZone(zone) as! CircularCollectionViewLayoutAttributes
    copiedAttributes.anchorPoint = self.anchorPoint
    copiedAttributes.angle = self.angle
    return copiedAttributes
  }
}
  1. 你需要一個anchorPoint屬性因為旋轉不是圍繞著item的中心點來的。
  2. 在設定angle屬性時,設置transform為旋轉,參數(shù)為angle radians。同時,你需要將右邊的item疊在左邊的item上,所以你設置了zIndex根據(jù)angle增大而增大。
  3. 最后,你需要重寫copyWithZone方法,這樣當你在copy該屬性時,會將你自定義的兩個屬性也copy進去。
    現(xiàn)在,回到CircularCollectionViewLayout 類,編寫layoutAttributesClass方法
override class func layoutAttributesClass() -> AnyClass {
  return CircularCollectionViewLayoutAttributes.self
}

這個方法將告知collectionView使用你自己創(chuàng)建的layoutAttributes類而不是默認的UICollectionViewLayoutAttributes。
為了保存所有的layout attributes的實例,創(chuàng)建一個數(shù)組attributesList在CircularCollectionViewLayout里

var attributesList = [CircularCollectionViewLayoutAttributes]()

準備Layout

當collectionView第一次顯示在屏幕上時,UICollectionViewLayout會執(zhí)行prepareLayout方法。這個方法在你調用invalidateLayout時也會執(zhí)行。
這也是layout過程中最重要的部分,因為這是你創(chuàng)建和保存所有l(wèi)ayout attributes的地方。

override func prepareLayout() {
  super.prepareLayout()
  
  let centerX = collectionView!.contentOffset.x + (CGRectGetWidth(collectionView!.bounds) / 2.0)
  attributesList = (0..<collectionView!.numberOfItemsInSection(0)).map { (i) 
      -> CircularCollectionViewLayoutAttributes in
    // 1
    let attributes = CircularCollectionViewLayoutAttributes(forCellWithIndexPath: NSIndexPath(forItem: I,
        inSection: 0))
    attributes.size = self.itemSize
    // 2
    attributes.center = CGPoint(x: centerX, y: CGRectGetMidY(self.collectionView!.bounds))
    // 3
    attributes.angle = self.anglePerItem*CGFloat(i)
    return attributes
  }
}

簡單點說,你對每個index path下的item做了一次遍歷,然后:

  1. 為每個index path創(chuàng)建CircularCollectionviewLayoutAttributes, 并設置它的大小。
  2. 將每個item都放在屏幕中央。
  3. 根據(jù)anglePerItem * I,將所有item旋轉。
    此外,你還需要重寫下面這些方法。并且這些方法將經(jīng)常被執(zhí)行,所以要注意它們的效率。
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
  return attributesList
}

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
  return attributesList[indexPath.row]
}

運行程序,你會發(fā)現(xiàn)得到了一個這樣子的collectionView


Anchor Point

回到prepareLayout方法,在centerX定義下面,加上

let anchorPointY = ((itemSize.height / 2.0) + radius) / itemSize.height

然后在map閉包里,return前,加上

attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)

然后在CircularCollectionViewCell里,重寫applyLayoutAttributes方法

override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
  super.applyLayoutAttributes(layoutAttributes)
  let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
  self.layer.anchorPoint = circularlayoutAttributes.anchorPoint
  self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)
}

這里,你用父類的方法來提供center,transform等等哪些默認已有的屬性,而自定義的屬性achorPoint需要我們自己手動作用在cell上。
再運行之后,


修復滾動效果

回到CircularCollectionViewLayout,然后在類的底部加上

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

這里返回true來告訴collectionView,每當滾動時,就重新計算layout,這也會調用prepareLayout方法。
下面,添加幾個參數(shù)

var angleAtExtreme: CGFloat {
  return collectionView!.numberOfItemsInSection(0) > 0 ? 
    -CGFloat(collectionView!.numberOfItemsInSection(0) - 1) * anglePerItem : 0
}
var angle: CGFloat {
  return angleAtExtreme * collectionView!.contentOffset.x / (collectionViewContentSize().width - 
    CGRectGetWidth(collectionView!.bounds))
}

然后在prepareLayout里,將這句

attributes.angle = (self.anglePerItem * CGFloat(i))

替換成

attributes.angle = self.angle + (self.anglePerItem * CGFloat(i))

這句代碼將angle添加給了每個item,再運行你會發(fā)現(xiàn),已經(jīng)成功啦。


優(yōu)化

在prepareLayout里,你給每個item都創(chuàng)建了attributes,但是并不是每個都顯示在了屏幕里,其實那些沒有顯示出來的item,你完全可以跳過計算。
添加下面這段代碼到prepareLayout方法的anchorPointY定義下面

// 1 
let theta = atan2(CGRectGetWidth(collectionView!.bounds) / 2.0, 
    radius + (itemSize.height / 2.0) - (CGRectGetHeight(collectionView!.bounds) / 2.0))
// 2
var startIndex = 0
var endIndex = collectionView!.numberOfItemsInSection(0) - 1 
// 3
if (angle < -theta) {
  startIndex = Int(floor((-theta - angle) / anglePerItem))
}
// 4
endIndex = min(endIndex, Int(ceil((theta - angle) / anglePerItem)))
// 5
if (endIndex < startIndex) {
  endIndex = 0
  startIndex = 0
}

你可以在原文看到這個算法是如何判定哪些item在屏幕內(nèi)的。
當你知道了這些以后,你需要在prepareLayout方法里將

attributesList = (0..<collectionView!.numberOfItemsInSection(0)).map { (i) 
    -> CircularCollectionViewLayoutAttributes in

替換成

attributesList = (startIndex...endIndex).map { (i) 
    -> CircularCollectionViewLayoutAttributes in

然后就大功告成啦!?。?/p>

最后附上我改寫的OC項目的鏈接: CircleCollectionViewDemo

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容