前言
想必大家工作中或多或少會(huì)遇到下圖樣式的UI需求吧

左對(duì)齊樣式UI
像這種cell長(zhǎng)度不固定,以此向右對(duì)齊排列的樣式UI可以說是很常見的
實(shí)現(xiàn)方式
一般的實(shí)現(xiàn)可能主要是分一下兩種:
- 1、一種是用button依次排列實(shí)現(xiàn),動(dòng)態(tài)計(jì)算text寬度,記錄之前一個(gè)button的位置,和當(dāng)前button的寬度,看是否最終會(huì)超出屏幕的右邊,一旦超出右邊,就換行到下一行
- 缺點(diǎn)
- 當(dāng)數(shù)據(jù)量多的時(shí)候,生成很多的button,不能對(duì)button進(jìn)行重用
- 每次生成一個(gè)button的時(shí)候都要計(jì)算位置,相對(duì)較麻煩
- 優(yōu)點(diǎn)
- 適合數(shù)據(jù)少的情況
- 缺點(diǎn)
- 2、采用collection view,依次從左到右進(jìn)行布局排列cell
- 優(yōu)點(diǎn)
- 數(shù)據(jù)量大的時(shí)候,能重用cell,減少cell數(shù)量,增高渲染性能
- 省去每次cell布局的位置計(jì)算
- 代碼復(fù)用,實(shí)現(xiàn)一個(gè)UICollectionViewFlowLayout的子類,拿到哪兒都能用
- 優(yōu)點(diǎn)
實(shí)現(xiàn)
我們這里實(shí)現(xiàn)主要采用第二種方式,實(shí)現(xiàn)的方式是自定義一個(gè)UICollectionViewFlowLayout的子類,在這個(gè)類里對(duì)cell布局進(jìn)行排列
主要代碼如下:
/// main method for layout cell
/// - Parameter indexPath: indexpath
/// - Returns: layouted UICollectionViewLayoutAttributes
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if let attr = calculatedAttrs[indexPath] { return attr }
guard let curAttr = super.layoutAttributesForItem(at: indexPath) else { return nil }
if isHorizontal {
// 如果滾動(dòng)方向是水平的話,就直接返回。這里的水平布局主要適合不換行的那種
calculatedAttrs[indexPath] = curAttr
return curAttr
}
// 下面主要針對(duì)滾動(dòng)方式是垂直方向的進(jìn)行布局,因?yàn)閷?shí)際開發(fā)中,絕大部分情況也是垂直滾動(dòng)方向
let sectionInset = calculateSectionInsetForItem(at: indexPath.section)
let layoutWidth = collectionView?.frame.width ?? 0 - sectionInset.left - sectionInset.right
if indexPath.item == 0 {
// 如果是當(dāng)前section的第一個(gè)元素,就直接設(shè)置x為sectionInset.left
curAttr.frame.origin.x = sectionInset.left
calculatedAttrs[indexPath] = curAttr
return curAttr
}
// 計(jì)算非第一個(gè)元素的frame布局
let prevIP = IndexPath(item: indexPath.item - 1, section: indexPath.section)
let prevRect = layoutAttributesForItem(at: prevIP)?.frame ?? .zero
let prevRectRightPoint = prevRect.origin.x + prevRect.size.width
let stretchedCurRect = CGRect(x: sectionInset.left,
y: curAttr.frame.origin.y,
width: layoutWidth,
height: curAttr.frame.size.height)
if !prevRect.intersects(stretchedCurRect) {
curAttr.frame.origin.x = sectionInset.left
calculatedAttrs[indexPath] = curAttr
return curAttr
}
curAttr.frame.origin.x = prevRectRightPoint + calculateMinimumInteritemSpacingForSection(at: indexPath.section)
calculatedAttrs[indexPath] = curAttr
return curAttr
}
- 這里我對(duì)水平滾動(dòng)方向也進(jìn)行了適配,不過水平滾動(dòng)方向主要適用于不換行的那種從左到右依次排列的樣式,比如如下示例圖:

4f9d619cc922474c998e1bbe8d11370a.jpeg
-
由于每次重用cell的時(shí)候,會(huì)再次重復(fù)計(jì)算cell的frame,為了減少重復(fù)冗余的計(jì)算,我進(jìn)行了如下的性能優(yōu)化:
- 這是常見的以空間換時(shí)間的解決方式
- 經(jīng)測(cè)試,這樣子優(yōu)化后,性能提升了將近90%
/// 用字典存儲(chǔ)已經(jīng)計(jì)算過的cell item,常見的以空間換時(shí)間方式
private lazy var calculatedAttrs = [IndexPath: UICollectionViewLayoutAttributes]()
- 在使用的時(shí)候,只需要將collection.collectionViewLayout的屬性設(shè)置為我們自定義的layout對(duì)象即可,具體代碼如下面的示例代碼:
private lazy var collectionView: UICollectionView = {
// instance ZLCollectionLeftAlignLayout
let defaultLayout = ZLCollectionLeftLayout()
defaultLayout.minimumLineSpacing = 10.0
defaultLayout.minimumInteritemSpacing = 10.0
defaultLayout.scrollDirection = .vertical
defaultLayout.sectionInset = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 20.0, right: 10.0)
// set collectionViewLayout to a instance of ZLCollectionLeftLayout
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: defaultLayout)
collectionView.backgroundColor = .magenta
collectionView.showsVerticalScrollIndicator = false
return collectionView
}()
- 之后就是在collection view的代理方法中設(shè)置每個(gè)cell的size就行了,具體示例代碼如下:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let w = CGFloat.random(in: 20.0 ... 50.0)
return CGSize(width: 30.0 + w, height: 25.0)
}
開源代碼地址
代碼開源到github上了,可以直接拿來使用