使用 UICollectionView 作錢包佈局界面,天然是自定製佈局,使用 UICollectionViewLayout
的子類git
指定大小 size, 間距 spacing, 每個格子的位置 frame 就出來了github
從頂部到底部,格子一個一個碼下來bash
把格子放在咱們想要的位置上,即手動指定他的 frameide
override public func prepare()
, 重寫準備方法通常能夠在這,把全部格子視圖的 frame 算出來佈局
frame 怎麼傳遞給對應的格子呢?ui
需建立對應的 UICollectionViewLayoutAttributes
,spa
每每會有一些信息要記錄,狀態要保留,就使用 UICollectionViewLayoutAttributes
的子類。好來添加屬性code
override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
, 重寫單個格子 item 的佈局方法把上一步準備好的 UICollectionViewLayoutAttributes
子類,提供過去,供 UICollectionView 的管理引擎使用orm
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
, 重寫格子視圖 UICollection
的佈局方法在這通常須要提供格子 item 、補充視圖 supplementary view 和裝飾視圖 decoration view 的佈局信息,cdn
本例子只涉及格子 item 的佈局, 把上一步準備好的 UICollectionViewLayoutAttributes
子類,提交給 UICollectionView 的管理引擎使用
這個佈局有兩個狀態,
沒有卡片被選中的狀態至關於初始狀態
該狀態的佈局信息,計算簡單,
指定第一張卡片的位置 frame, 餘下卡片的位置,就都肯定了
就是縱座標有變化,y: titleHeight * CGFloat(index)
fileprivate func setNoSelect(attribute:CardLayoutAttributes) {
guard let collection = collectionView else {
return
}
let noneIdx = Int(collection.contentOffset.y/titleHeight)
if noneIdx < 0 {
return
}
attribute.isExpand = false
let index = attribute.zIndex
var currentFrame = CGRect(x: collection.frame.origin.x, y: titleHeight * CGFloat(index), width: cellSize.width, height: cellSize.height)
if index == noneIdx{
attribute.frame = CGRect(x: currentFrame.origin.x, y: collection.contentOffset.y, width: cellSize.width, height: cellSize.height)
}
else if index <= noneIdx, currentFrame.maxY > collection.contentOffset.y{
currentFrame.origin.y -= (currentFrame.maxY - collection.contentOffset.y )
attribute.frame = currentFrame
}
else {
attribute.frame = currentFrame
}
}
複製代碼
這裏使用了 UICollectionViewLayoutAttributes
的子類,
添加了一個卡片是否展開的屬性 isExpand
本例子中的 item , 添加了一個平移手勢,isExpand
用於手勢的交互
class CardLayoutAttributes: UICollectionViewLayoutAttributes {
var isExpand = false
override func copy(with zone: NSZone? = nil) -> Any {
let attribute = super.copy(with: zone) as! CardLayoutAttributes
attribute.isExpand = isExpand
return attribute
}
}
複製代碼
這裏使用的計算方式是,把選中的卡片放在中間固定的位置,即肯定了選中卡片的 y
座標爲 collection.contentOffset.y + offsetSelected
這等於選中的編號 selectedIdx 的位置 frame, 肯定了
中間的位置肯定,而後計算兩邊
一邊從 selectedIdx - 1 到 0, 另外一邊從 selectedIdx + 1 到結尾,算出每個格子對應的 y
座標,就肯定了其 frame
fileprivate func calculate(for attributes: [CardLayoutAttributes], choose selectedIP: IndexPath) -> [CGRect]{
guard let collection = collectionView else {
return []
}
let noneIdx = Int(collection.contentOffset.y / titleHeight)
if noneIdx < 0 {
return []
}
let x = collection.frame.origin.x
var selectedIdx = 0
for attr in attributes{
if attr.indexPath == selectedIP{
break
}
selectedIdx += 1
}
var frames = [CGRect](repeating: .zero, count: attributes.count)
// Edit here
let offsetSelected: CGFloat = 100
let marginBottomSelected: CGFloat = 10
frames[selectedIdx] = CGRect(x: x, y: collection.contentOffset.y + offsetSelected, width: cellSize.width, height: cellSize.height)
if selectedIdx > 0{
for i in 0...(selectedIdx-1){
frames[selectedIdx - i - 1] = CGRect(x: x, y: frames[selectedIdx].origin.y - titleHeight * CGFloat(i + 1), width: cellSize.width, height: cellSize.height)
}
}
if selectedIdx < (attributes.count - 1){
for i in (selectedIdx + 1)...(attributes.count - 1){
frames[i] = CGRect(x: x, y: frames[selectedIdx].origin.y + marginBottomSelected + titleHeight * CGFloat(i - selectedIdx - 1) + cellSize.height, width: cellSize.width, height: cellSize.height)
}
}
return frames
}
複製代碼
UICollecionView
的自定製佈局調用 invalidateLayout()
,就會把前面三個方法,從 prepare()
開始, 再走一遍
fileprivate var _selectPath: IndexPath? {
didSet {
self.collectionView!.isScrollEnabled = (_selectPath == nil)
}
}
public var selectPath: IndexPath? {
set {
_selectPath = (_selectPath == newValue) ? nil : newValue
self.collectionView?.performBatchUpdates({
self.invalidateLayout()
}, completion: nil)
} get {
return _selectPath
}
}
複製代碼
UICollectionView 的格子層層疊疊,下面的碼在上層的上方,經過 layer.zPosition
來保證