CollectionViewCell 自動(dòng)高度
寫在前面:
UITableViewCell的自動(dòng)高度很方便往果,開發(fā)中很多時(shí)候首選tableView的原因也是因?yàn)檫@個(gè)钾埂,可以減少很多高度或者動(dòng)態(tài)高度的計(jì)算過程兢孝。
CollectionViewCell 其實(shí)也是可以自動(dòng)高度的, 需要重寫實(shí)現(xiàn)一個(gè)方法func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
1. CollectionViewCell實(shí)現(xiàn)自動(dòng)高度
CollctionViewCell 要實(shí)現(xiàn)自動(dòng)高度,除了UI控件在布局上要像在TableViewCell一樣做到用約束撐開contentView之外髓堪,還要實(shí)現(xiàn)func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
方法屯仗, 實(shí)現(xiàn)這個(gè)方法的目的其實(shí)也是通過手動(dòng)計(jì)算的方式來返回真實(shí)的高度搞坝。
比如:假如collectionViewCell中有一個(gè)textView(或者label什么的), 現(xiàn)在需求是這個(gè)cell的高度要隨著textView中的自動(dòng)換行而增大,那么在這個(gè)方法中可以這樣計(jì)算:
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
var newFrame = layoutAttributes.frame
newFrame.size.height = (textView.text?.isSpaceOrEmpty ?? true) ? 0 : ceil(size.height)
layoutAttributes.frame = newFrame
return layoutAttributes
}
這樣在textView字?jǐn)?shù)發(fā)生變化時(shí)就會(huì)調(diào)用這個(gè)方法魁袜,實(shí)時(shí)計(jì)算實(shí)際高度并重新布局了桩撮。
2. IGListKit中的CollectionViewCell的高度計(jì)算
在IGListKit中實(shí)現(xiàn)sectionController時(shí),必要實(shí)現(xiàn)sizeForItem
這個(gè)方法峰弹,在這個(gè)方法里對(duì)每種cell手動(dòng)計(jì)算它的實(shí)際高度并返回店量,有時(shí)候也要配合上面自動(dòng)布局需要實(shí)現(xiàn)的方法共同計(jì)算并返回真實(shí)高度。
比如鞠呈,CollectionViewCell中有一個(gè)支持多行的label, 在sectionController的sizeForItem
中首先要手動(dòng)計(jì)算這些字所占高度融师,再加上label上下間隙高度。
計(jì)算字所占高度通骋狭撸可通知?jiǎng)?chuàng)建一個(gè)模擬label的實(shí)際計(jì)算旱爆,注意這個(gè)模擬label的字體以及樣式要和真實(shí)的label保持一致。
static func height(text: String,
width: CGFloat,
topPadding: CGFloat = 20,
bottomPadding: CGFloat = 20) -> CGFloat {
let fakeLabel = XOLabel(frame: CGRect(x: 0,
y: 0,
width: width - 40,
height: .greatestFiniteMagnitude))
fakeLabel.numberOfLines = 0
fakeLabel.font = .caption1
fakeLabel.text = text
fakeLabel.sizeToFit()
return fakeLabel.bounds.size.height + topPadding + bottomPadding
}
注意: 因?yàn)檫@個(gè)cell中的實(shí)際label.numbersOfLines = 0
窘茁,需要實(shí)現(xiàn)自動(dòng)高度怀伦,所以依然要實(shí)現(xiàn)func preferredLayoutAttributesFitting
方法才能實(shí)現(xiàn)完全撐開cell。
算完之后在sizeForItem
中就可以為這個(gè)cell返回實(shí)際高度了山林。
3. CollectionView的自動(dòng)高度
有時(shí)候需要實(shí)現(xiàn)在tableViewCell中嵌入collectionView, 雖然tableViewCell可以實(shí)現(xiàn)自動(dòng)高度房待,但因?yàn)閏ollectionView是可以滾動(dòng)的,雖然可以像textView一樣禁掉滾動(dòng)屬性驼抹,但畢竟是collectionView, 有時(shí)候加載的東西比較多桑孩,實(shí)現(xiàn)不了像textView一樣實(shí)時(shí)更新高度。
除非CollectionViewCell中的item大小是可以確定的砂蔽,這種情況下可以實(shí)現(xiàn)整個(gè)tableViewCell的自動(dòng)高度洼怔。
如果collectionViewCell的item大小不確定署惯,有什么辦法實(shí)現(xiàn)tableViewCell的自動(dòng)高度呢左驾?
網(wǎng)上查到的方法是大概是這樣說的:在TabelViewCell的layoutSubviews重新方法中加入以下代碼
self.contentView.frame = bounds // 先為contentView設(shè)置frame,使得子視圖有布局依據(jù),使得contentView不會(huì)被忽略而被線程渲染。
self.collectionView.reloadData()
let contentSize = collectionView.contentSize
self.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
這樣看起來挺合理的极谊,但實(shí)現(xiàn)運(yùn)行起來出問題了诡右,高度不對(duì),還出現(xiàn)死循環(huán)轻猖》牵可能是不適用于我當(dāng)時(shí)的demo。
所幸我需要實(shí)現(xiàn)的collectionView的item比較單一咙边,這樣可以實(shí)際去計(jì)算實(shí)際高度猜煮,于是將所有間隙找出次员,一個(gè)一個(gè)item計(jì)算其寬度,當(dāng)前行放不下了就換行+高:
private func heightForCell(withFilters filters: [String]) -> CGFloat {
var widthRemaining = LayoutParameter.collectionWidth
var height: CGFloat = LayoutParameter.collectionViewTopPadding
let fakeLabel = XOLabel()
fakeLabel.font = .body
filters.enumerated().forEach { (index, filter) in
fakeLabel.text = filter
if index == 0 {
let firstLineHeightOfItems = LayoutParameter.itemHeight
height += firstLineHeightOfItems
}
fakeLabel.sizeToFit()
let itemWidth = LayoutParameter.labelLeftPad + fakeLabel.width() + LayoutParameter.labelRightPad
//當(dāng)當(dāng)前行不足以再放多一個(gè)item時(shí)換行王带,+高度
if widthRemaining - itemWidth - LayoutParameter.itemSpace <= 0 {
height += (LayoutParameter.verticalSpace + LayoutParameter.itemHeight)
widthRemaining = LayoutParameter.collectionWidth
// 換行結(jié)束將行寬還原
}
widthRemaining -= (LayoutParameter.itemSpace + itemWidth)
}
return height
}
這樣就可以計(jì)算出整個(gè)collectionView的高度了淑蔚,只要間隙的值準(zhǔn)備,計(jì)算的結(jié)果幾乎是正確的愕撰。
4. collectionView 自定義布局
collectionView 橫向布局刹衫、縱向布局、 items 左右對(duì)齊搞挣、上下對(duì)齊
1. items 左對(duì)齊
collectionView items 默認(rèn)是分散對(duì)齊的带迟,如果需要實(shí)現(xiàn)類似以下的左對(duì)齊效果需要自定義布局。
左對(duì)齊的實(shí)現(xiàn)方式比較簡(jiǎn)單囱桨,直接重寫UICollectionViewFlowLayout
下的layoutAttributesForElements( )
public final class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0 //設(shè)置負(fù)值仓犬,保證第一次滿足條件
attributes?.forEach { layoutAttribute in //item
if layoutAttribute.frame.origin.y >= maxY { //如果還沒換行則不滿足條件,換行了才滿足條件使得leftMargin回到0
leftMargin = sectionInset.left
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing //左對(duì)齊
maxY = max(layoutAttribute.frame.maxY, maxY)
}
return attributes
}
}
//然后在collectionView的layout中可能這樣使用
private let layout: LeftAlignedCollectionViewFlowLayout = {
let layout = LeftAlignedCollectionViewFlowLayout()
layout.scrollDirection = .vertical
return layout
}( )
collectionView.collectionViewLayout = layout
//collectionView.isScrollEnabled = false
2. 右對(duì)齊
右對(duì)齊類似蝇摸,只是計(jì)算中有點(diǎn)點(diǎn)不一樣
public final class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var rightMargin = rect.maxX
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.origin.y >= maxY {
rightMargin = rect.maxX - 20
}
layoutAttribute.frame.origin.x = rightMargin - layoutAttribute.frame.width
rightMargin -= layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY, maxY)
}
return attributes
}
}
3. 底部對(duì)齊
在做一些聊天頁面時(shí)婶肩,當(dāng)剛開始建立對(duì)話,只有一兩個(gè)cell時(shí)貌夕,希望像Slack 軟件一樣做到底部對(duì)齊律歼,可以這樣做。
public final class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
if let items = attributes,
!items.isEmpty,
let lastItem = items.reversed().first,
let collectionView = collectionView,
lastItem.frame.maxY < collectionView.bounds.maxY {
let bottomPadding = collectionView.bounds.maxY - lastItem.frame.maxY
items.forEach {
$0.frame.origin.y += bottomPadding
}
return items
} else {
return attributes
}
}
}