在写商品搜索功能,包括历史搜索记录和热门搜索,这里面用到了文字标签自动排版,所以研究了一下UICollectionViewLayout。
UICollectionViewLayout是对CollectionView的布局和行为进行描述的类,这是CollectionView和TableView的重要区别,通过这个类,可以根据需要调整cell的布局,一般瀑布流用collectionView来实现。
UICollectionViewLayout是一个需要子类化的抽象基类,布局对象(layout object)决定了cell,Supplementary Views(追加视图)和Decoration Views(装饰视图) 在collection view 内部的位置,在collection view获取的时候提供这些布局信息。布局对象主要用来提供位置和状态信息,它并不负责创建视图,cell、追加视图、装饰视图是由collection view的数据源(data source)创建的。
自定义UICollectionViewLayout
自定义的layout要继承UICollectionViewLayout类,然后重载下列方法:
- (CGSize)collectionViewContentSize;
1.返回值表示所有内容的尺寸,决定了collection view的滚动范围
2.自动调用,子类必须重载该方法
- (void)prepareLayout;
1.首次布局和之后重新布局的时候会调用,并不会每次滑动都调用
2.当数据源变化时也会调用
3.自动调用
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
1.返回rect(可见范围内)中所有元素的布局属性,当collection view 滑动时会再调用这个方法获取布局属性
2.UICollectionViewLayoutAttributes包含cell或追加视图或装饰视图的布局信息
3.在创建UICollectionViewLayoutAttributes对象时,要根据视图是cell还是supplementary还是decoration来创建attributes对象
layoutAttributesForCellWithIndexPath:
layoutAttributesForSupplementaryViewOfKind:withIndexPath:
layoutAttributesForDecorationViewOfKind:withIndexPath:
4.自动调用,子类必须重载该方法
-(UICollectionViewLayoutAttributes )layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath
1.返回对应于indexPath的位置的cell的布局属性
2.不会自动调用,在需要的时候用UICollectionViewLayout对象触发该方法,不需要的话可以不重写该方法
-(UICollectionViewLayoutAttributes )layoutAttributesForSupplementaryViewOfKind:(NSString )kind atIndexPath:(NSIndexPath *)indexPath
1.返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
2.在需要的地方,用UICollectionViewLayout对象调用该方法
-(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString)decorationViewKind atIndexPath:(NSIndexPath )indexPath
1.返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
2.在需要的地方,用UICollectionViewLayout对象调用该方法
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
项目中的应用
我这个搜索模块中用的collection view是有分组的,所以自定义的UICollectionViewLayout中,要计算cell和Supplementary Views(追加视图)的布局。
在prepareLayout中,把所有元素的属性计算好,存到attributesArray数组中。
- (void)prepareLayout {
[super prepareLayout];
attributesArray = [NSMutableArray array];
......(省略^_^)
self.scrollDirection = UICollectionViewScrollDirectionVertical;
NSInteger section = [self.collectionView numberOfSections];
for (int i = 0; i < section; i++) {
NSInteger item = [self.collectionView numberOfItemsInSection:i];
if (item) {
// 分组头视图的布局属性 加入数组中,才起作用
UICollectionViewLayoutAttributes *sectionAtt = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:i]];
[attributesArray addObject:sectionAtt];
}
for (int j = 0; j < item; j++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
// 每个cell的布局属性 加入数组中
UICollectionViewLayoutAttributes *att = [self layoutAttributesForItemAtIndexPath:indexPath];
[attributesArray addObject:att];
}
}
}
prepareLayout计算出的attributesArray,包含所有视图的布局属性,在以下方法中返回。
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
return attributesArray;
}
重写layoutAttributesForSupplementaryViewOfKind:atIndexPath:,在prepareLayout方法中调用了这个方法,其实追加视图布局属性就是在这个方法中计算出来的。
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attri = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath];
/*
[super layoutAttributesForSupplementaryViewOfKind:elementKind atIndexPath:indexPath];
通过super 创建的对象是nill
*/
if (orgin.lineX > contentInsets.left) {
orgin.totalY += itemHeight+lineSpace;
}
attri.frame = CGRectMake(0, orgin.totalY, self.collectionView.width, sectionHeight);
orgin.totalY += sectionHeight;
return attri;
}
在下面这个方法中,计算了indexPath位置的cell的布局。同样在prepareLayout中调用了这个方法,将所有item的UICollectionViewLayoutAttributes加到数组中。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attr = [super layoutAttributesForItemAtIndexPath:indexPath];
/*这里用[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]创建attr也是可以的。这个地方用super 方法创建的attr不是nill,但是上面那个追加视图里面用super 就不可以,目前还在分析原因,希望知道的简友,可以告诉我^_^*/
// 进入下一个分组
if (orgin.section != indexPath.section) {
orgin.section = indexPath.section;
orgin.lineNumber ++;
orgin.lineX = contentInsets.left;
}
//获得当前item的标题
NSString *title = [self.dataSource titleForLabelAtIndexPath:indexPath];
CGSize size = [self sizeWithTitle:title font:titleFont];
CGFloat itemWidth = size.width+itemMargin;
// 标签的最大宽度
if (itemWidth > CGRectGetWidth(self.collectionView.frame)-(contentInsets.left+contentInsets.right)) {
itemWidth = CGRectGetWidth(self.collectionView.frame)-(contentInsets.left+contentInsets.right);
}
if (itemWidth > CGRectGetWidth(self.collectionView.frame)-contentInsets.right-orgin.lineX) {
orgin.lineNumber ++;
orgin.lineX = contentInsets.left;
orgin.totalY += itemHeight+lineSpace;
}
CGFloat itemOrginX = orgin.lineX;
CGFloat itemOrginY = orgin.totalY;
attr.frame = CGRectMake(itemOrginX, itemOrginY, itemWidth, itemHeight);
orgin.lineX += itemWidth+itemSpace;
return attr;
}
origin对象是一个结构体,lineX记录了即将布局的item的x值;lineNumber记录即将布局的item是第几行,之前用这个值计算y值,现在没什么用了;totalY是即将布局的item的y值,最后totalY就是最后一个item的y值;section记录当前分组。
typedef struct currentOrigin {
CGFloat lineX;
NSInteger lineNumber;
NSInteger section; // 记录当前分组
CGFloat totalY; // collectionView 内容的最大Y值
}currentOrigin;
- (CGSize)sizeWithTitle:(NSString *)title font:(UIFont *)font {
CGRect rect = [title boundingRectWithSize:CGSizeMake(1000, itemHeight) options:NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesFontLeading|NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil];
return rect.size;
}
// 返回collection view 内容的尺寸,决定了collection是否可以滑动
- (CGSize)collectionViewContentSize {
CGFloat sizeHeight = orgin.lineX > contentInsets.left ? orgin.totalY+ itemHeight+lineSpace : orgin.totalY;
return CGSizeMake(self.collectionView.width, sizeHeight);
}