iOS UICollectionViewLayout 自定義布局基礎(chǔ)

CollectionView 相關(guān)內(nèi)容:

1. iOS 自定義圖片選擇器 3 - 相冊列表的實(shí)現(xiàn)
2. UICollectionView自定義布局基礎(chǔ)
3. UICollectionView自定義拖動(dòng)重排
4. iOS13 中的 CompositionalLayout 與 DiffableDataSource
5. iOS14 中的UICollectionViewListCell麸粮、UIContentConfiguration 以及 UIConfigurationState

UICollectionView在iOS開發(fā)中是一大利器檬姥,之前在文章【iOS 自定義圖片選擇器 3 - 相冊列表的實(shí)現(xiàn)(UICollectionView)】中有對系統(tǒng)提供的 UICollectionViewFlowLayout 有簡單介紹和使用磅废,不了解的朋友也可先看看羞迷。這一篇為基礎(chǔ)介紹,若讀者急于尋找解決問題的答案或工具類文檔斗锭,建議直接查看官方文檔

前言

UICollectionView 可以實(shí)現(xiàn)很多酷炫的布局,網(wǎng)上很多文章中有很多各種布局的展示免姿,這里就不去找演示圖了(因?yàn)槲覒校?br> 本篇文章我們以一個(gè)簡單的效果來對UICollectionView的布局有一個(gè)基礎(chǔ)的認(rèn)識(shí),打牢基礎(chǔ)后就可以自由發(fā)揮啦榕酒,效果如下圖:

image

自定義布局需要實(shí)現(xiàn)UICollectionViewLayout的子類胚膊,我們看看在UICollectionViewLayout中有些什么是我們現(xiàn)在要用到的:

//CollectionView會(huì)在初次布局時(shí)首先調(diào)用該方法
//CollectionView會(huì)在布局失效后、重新查詢布局之前調(diào)用此方法
//子類中必須重寫該方法并調(diào)用超類的方法
-(void)prepareLayout;

//子類必須重寫此方法想鹰。
//并使用它來返回CollectionView視圖內(nèi)容的寬高紊婉,
//這個(gè)值代表的是所有的內(nèi)容的寬高,并不是當(dāng)前可見的部分辑舷。
//CollectionView將會(huì)使用該值配置內(nèi)容的大小來促進(jìn)滾動(dòng)喻犁。
- (CGRect)collectionViewContentSize;

// UICollectionView 調(diào)用以下四個(gè)方法來確定布局信息
- (NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect; // return an array layout attributes instances for all the views in the given rect
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;

//當(dāng)Bounds改變時(shí),返回YES使CollectionView重新查詢幾何信息的布局
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;

以上提到的方法就是本文所用到的主要方法何缓,也是CollectionView自定義布局的幾個(gè)核心方法肢础。實(shí)際上CollectionViewLayout所提供的方法遠(yuǎn)不止這些,例如還有:

//用于控制滾動(dòng)的方法
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity;
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset NS_AVAILABLE_IOS(7_0); 

//iOS9之后碌廓,拖動(dòng)的相關(guān)控制
- (NSIndexPath *)targetIndexPathForInteractivelyMovingItem:(NSIndexPath *)previousIndexPath withPosition:(CGPoint)position NS_AVAILABLE_IOS(9_0);
- (UICollectionViewLayoutAttributes *)layoutAttributesForInteractivelyMovingItemAtIndexPath:(NSIndexPath *)indexPath withTargetPosition:(CGPoint)position NS_AVAILABLE_IOS(9_0);

//插入或刪除的相關(guān)控制
- (nullable UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath;
- (nullable UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath;

更多的方法大家可以自行查閱文檔传轰,本來是想把文檔翻譯下的,結(jié)果發(fā)現(xiàn)早就有人做了谷婆,鏈接在這里慨蛙。



先想一想

開始之前需要準(zhǔn)備一個(gè)帶有 CollectionView 的頁面,實(shí)現(xiàn)一個(gè) UICollectionViewLayout 的子類用于實(shí)現(xiàn)我們的自定義布局纪挎,并將其賦值給 CollectionView:

現(xiàn)在要實(shí)現(xiàn)的布局為“等高不等寬的垂直流式布局”期贫,在【iOS 自定義圖片選擇器 3 - 相冊列表的實(shí)現(xiàn)(UICollectionView)】中我們用到了系統(tǒng)實(shí)現(xiàn)的流式布局UICollectionViewFlowLayout, 而我們現(xiàn)在實(shí)現(xiàn)的這個(gè)布局與其有一定相似:

  1. 流式布局
  2. UICollectionViewFlowLayout類中的itemSize, SectionInset等參數(shù)配置我們也需要用到,用于布局信息的計(jì)算廷区。

有相似點(diǎn)唯灵,就可以仿照其結(jié)構(gòu)進(jìn)行一定程度的模仿,模仿之前我們先觀摩下FlowLayout的頭文件:

@property (nonatomic) CGFloat minimumLineSpacing;  //最小行間距
@property (nonatomic) CGFloat minimumInteritemSpacing; //最小item間距
@property (nonatomic) CGSize itemSize; //item大小
@property (nonatomic) CGSize estimatedItemSize //預(yù)設(shè)item大小 NS_AVAILABLE_IOS(8_0); // defaults to CGSizeZero - setting a non-zero size enables cells that self-size via -preferredLayoutAttributesFittingAttributes:
@property (nonatomic) UICollectionViewScrollDirection scrollDirection; // 默認(rèn)為 UICollectionViewScrollDirectionVertical
@property (nonatomic) CGSize headerReferenceSize; //header size
@property (nonatomic) CGSize footerReferenceSize; //footer size
@property (nonatomic) UIEdgeInsets sectionInset; //section的內(nèi)邊距

//iOS11 后新增的方法隙轻,可用于約束CollectionViewsection來適配SafeAre(劉海屏埠帕, 例如你橫平時(shí)劉海屏有部分遮擋的情況。)
/// The reference boundary that the section insets will be defined as relative to. Defaults to `.fromContentInset`.
/// NOTE: Content inset will always be respected at a minimum. For example, if the sectionInsetReference equals `.fromSafeArea`, but the adjusted content inset is greater that the combination of the safe area and section insets, then section content will be aligned with the content inset instead.
@property (nonatomic) UICollectionViewFlowLayoutSectionInsetReference sectionInsetReference API_AVAILABLE(ios(11.0), tvos(11.0)) API_UNAVAILABLE(watchos);

// 當(dāng)返回YES時(shí)玖绿,將會(huì)懸浮對應(yīng)的所有Header/Footer敛瓷。
@property (nonatomic) BOOL sectionHeadersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);
@property (nonatomic) BOOL sectionFootersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);

都是一些最基礎(chǔ)的配置參數(shù),我們實(shí)現(xiàn)的效果還沒有header斑匪,footer呐籽,也降低了我們配置布局的復(fù)雜度,這樣看下來上面近一半的參數(shù)我們的布局都用不到,當(dāng)然啦有時(shí)間還是寫的健壯一點(diǎn)狡蝶,萬一哪天產(chǎn)品想加100個(gè)header呢庶橱?

現(xiàn)在我們知道了相似的地方,那么不同點(diǎn)在哪里呢贪惹?

等高不等寬

不等寬 ” 意味著我們的寬度極有可能來自于數(shù)據(jù)的寬度苏章,這種不固定的因素我們需要把其拋給調(diào)用者動(dòng)態(tài)配置,這里使用代理奏瞬,Block都是可以的枫绅,根據(jù)項(xiàng)目規(guī)范來吧。



現(xiàn)在開始實(shí)現(xiàn)我們那“等高不等寬”的布局吧

了解了相同點(diǎn)不同點(diǎn)硼端,我們可以把空白的布局頭文件充實(shí)下了:

@interface RJHorizontalEqulHeightFlowLayout : UICollectionViewLayout
@property (assign, nonatomic) CGFloat itemHeight;
@property (assign, nonatomic) CGFloat itemSpace;
@property (assign, nonatomic) CGFloat lineSpace;
@property (assign, nonatomic) UIEdgeInsets sectionInsets;

/**
 配置item的寬度
 */
- (void)configItemWidth:(CGFloat (^)(NSIndexPath * indexPath, CGFloat height))widthBlock;

@end

現(xiàn)在先不要慌并淋,在實(shí)現(xiàn)文件中,我們先實(shí)現(xiàn)那倆要求必須實(shí)現(xiàn)的方法 prepareLayout 與 layoutAttributesForElementsInRect

prepareLayout 在初始化以及每次失效后珍昨、重新查詢布局之前都會(huì)調(diào)用县耽。那么我們的布局初始化,改變在這里配置最為理想曼尊,而 layoutAttributesForElementsInRect 是返回了一個(gè)布局信息的集合酬诀。


是的脏嚷,我們需要一個(gè)集合來保存我們的布局信息骆撇,且需要記錄上一個(gè)item的布局信息的相關(guān)參數(shù)。

@property (assign, nonatomic) CGFloat currentY;   //當(dāng)前Y值
@property (assign, nonatomic) CGFloat currentX;   //當(dāng)前X值
@property (copy, nonatomic) WidthBlock widthComputeBlock;   //外包的寬度Block

@property (strong, nonatomic) NSMutableArray * attrubutesArray;   //所有元素的布局信息


那么單個(gè)的item布局信息在哪里配置呢父叙?

 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;

這其實(shí)也是一個(gè)必須實(shí)現(xiàn)的方法神郊,沒有哪個(gè)CollectionView會(huì)一直為空吧?相應(yīng)的趾唱,若我們的collectionView有header涌乳、footer的話,也必須要重載與之對應(yīng)的方法甜癞。

我們的布局只需要循環(huán)所有元素夕晓,并根據(jù)collectionView的大小,高度悠咱,以及動(dòng)態(tài)的寬度確定每一個(gè)元素的位置蒸辆,大小等信息,每個(gè)元素的信息都包含在一個(gè)UICollectionViewLayoutAttributes中,它所包含的參數(shù)并不多析既,都是基礎(chǔ)的配置參數(shù)躬贡,可自行查看。我們現(xiàn)在已經(jīng)做好了實(shí)現(xiàn)一個(gè)自定義布局所有最基礎(chǔ)的準(zhǔn)備眼坏,詳細(xì)的實(shí)現(xiàn)如下:

- (void)prepareLayout {
    [super prepareLayout];
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    //初始化首個(gè)item位置
    _currentY = _sectionInsets.top;
    _currentX = _sectionInsets.left;
    _attrubutesArray = [NSMutableArray array];
    //得到每個(gè)item屬性并存儲(chǔ)
    for (NSInteger i = 0; i < count; i ++) {
        NSIndexPath * indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes * attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
        [_attrubutesArray addObject:attributes];
    }
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    //獲取寬度
    CGFloat contentWidth = self.collectionView.frame.size.width - _sectionInsets.left - _sectionInsets.right;
    
    //通過indexpath創(chuàng)建一個(gè)item屬性
    UICollectionViewLayoutAttributes * temp = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    //計(jì)算item寬
    CGFloat itemW = 0;
    if (_widthComputeBlock) {
        itemW = self.widthComputeBlock(indexPath, _itemHeight);
        //約束寬度最大值
        if (itemW > contentWidth) {
            itemW = contentWidth;
        }
    } else {
        NSAssert(YES, @"請實(shí)現(xiàn)計(jì)算寬度的block方法");
    }
    
    //計(jì)算item的frame
    CGRect frame;
    frame.size = CGSizeMake(itemW, _itemHeight);
    
    //檢查坐標(biāo)
    if (_currentX + frame.size.width > contentWidth) {
        _currentX = _sectionInsets.left;
        _currentY += (_itemHeight + _lineSpace);
    }
    //設(shè)置坐標(biāo)
    frame.origin = CGPointMake(_currentX, _currentY);
    temp.frame = frame;
    
    //偏移當(dāng)前坐標(biāo)
    _currentX += frame.size.width + _itemSpace;
    return temp;
}


- (CGSize)collectionViewContentSize {
    return CGSizeMake(1,
                      _currentY + _itemHeight + _sectionInsets.bottom);
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    return _attrubutesArray;
}

這樣就達(dá)成效果了拂玻。


結(jié)語:

自定義布局難點(diǎn)不在于其本身,難點(diǎn)在于各種自定義布局的實(shí)現(xiàn)方法,很多很酷炫的動(dòng)畫還要用到各種數(shù)學(xué)函數(shù)檐蚜,若再在上面加點(diǎn)手勢事件呢魄懂,再加個(gè)手勢動(dòng)畫呢。

本篇文章僅實(shí)現(xiàn)了一個(gè)最簡單的自定義布局闯第。若讀著想要加深下理解逢渔,可以做如下練習(xí):

  1. 等寬不等高的垂直流式布局(寬高都不等呢)
  2. 居中放大的banner滾動(dòng)效果布局(加入自動(dòng)滾動(dòng)以及相關(guān)手勢)
  3. 圓環(huán)布局(加入手勢滾動(dòng),外滑刪除)
  4. 球體布局

下一篇文章會(huì)在此文章的項(xiàng)目基礎(chǔ)上進(jìn)行 拖動(dòng)重排 的探索乡括。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載肃廓,如需轉(zhuǎn)載請通過簡信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末诲泌,一起剝皮案震驚了整個(gè)濱河市盲赊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌敷扫,老刑警劉巖哀蘑,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異葵第,居然都是意外死亡绘迁,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門卒密,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缀台,“玉大人,你說我怎么就攤上這事哮奇√鸥” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵鼎俘,是天一觀的道長哲身。 經(jīng)常有香客問我,道長贸伐,這世上最難降的妖魔是什么勘天? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮捉邢,結(jié)果婚禮上脯丝,老公的妹妹穿的比我還像新娘。我一直安慰自己歌逢,他們只是感情好巾钉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秘案,像睡著了一般砰苍。 火紅的嫁衣襯著肌膚如雪潦匈。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天赚导,我揣著相機(jī)與錄音茬缩,去河邊找鬼。 笑死吼旧,一個(gè)胖子當(dāng)著我的面吹牛凰锡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播圈暗,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掂为,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了员串?” 一聲冷哼從身側(cè)響起勇哗,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寸齐,沒想到半個(gè)月后欲诺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡渺鹦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年扰法,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片毅厚。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡塞颁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卧斟,到底是詐尸還是另有隱情殴边,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布珍语,位于F島的核電站,受9級(jí)特大地震影響竖幔,放射性物質(zhì)發(fā)生泄漏板乙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一拳氢、第九天 我趴在偏房一處隱蔽的房頂上張望募逞。 院中可真熱鬧,春花似錦馋评、人聲如沸放接。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纠脾。三九已至玛瘸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間苟蹈,已是汗流浹背糊渊。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留慧脱,地道東北人渺绒。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像菱鸥,于是被迫代替她去往敵國和親宗兼。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容