使用 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
来保证