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ā)揮啦榕酒,效果如下圖:
自定義布局需要實(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è)布局與其有一定相似:
- 流式布局
- 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í):
- 等寬不等高的垂直流式布局(寬高都不等呢)
- 居中放大的banner滾動(dòng)效果布局(加入自動(dòng)滾動(dòng)以及相關(guān)手勢)
- 圓環(huán)布局(加入手勢滾動(dòng),外滑刪除)
- 球體布局
下一篇文章會(huì)在此文章的項(xiàng)目基礎(chǔ)上進(jìn)行 拖動(dòng)重排 的探索乡括。