Collection View

Flowlayout设置元素大小的方法在UICollectionViewDelegateFlowLayout代理里

切换layout

- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated; 
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0); 

触发layout切换 diffable-data-source教程

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  super.viewWillTransition(to: size, with: coordinator)
  coordinator.animate(alongsideTransition: { context in
                                            self.collectionView.collectionViewLayout.invalidateLayout()
                                           }, completion: nil)
}

layout和动画

//这个UICollectionViewTransitionLayout对象管理动画的相关属性,我们可以进行设置
- (UICollectionViewTransitionLayout *)startInteractiveTransitionToCollectionViewLayout:(UICollectionViewLayout *)layout completion:(nullable UICollectionViewLayoutInteractiveTransitionCompletion)completion NS_AVAILABLE_IOS(7_0);
//准备好动画设置后,我们需要调用下面的方法进行布局动画的展示,之后会调用上面方法的block回调
- (void)finishInteractiveTransition NS_AVAILABLE_IOS(7_0);
//调用这个方法取消上面的布局动画设置,之后也会进行上面方法的block回调
- (void)cancelInteractiveTransition NS_AVAILABLE_IOS(7_0);

//下面两个方法获取item或者头尾视图的layout属性,这个UICollectionViewLayoutAttributes对象
//存放着布局的相关数据,可以用来做完全自定义布局
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;

一个环形布局的例子

// 返回collectionView的内容尺寸
-(CGSize)collectionViewContentSize
{
    return [self collectionView].frame.size;
}

// 返回对应于indexPath的位置的cell的布局属性
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
    attributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
    attributes.center = CGPointMake(_center.x + _radius * cosf(2 * path.item * M_PI / _cellCount),
                                    _center.y + _radius * sinf(2 * path.item * M_PI / _cellCount));
    return attributes;
}
// 上面的方法对设置每一个indexPath,这个方法确定rect里面要返回哪些元素的布局描述,因为是环形布局,所以是能显示所有元素的,所以这个例子没能很好地演示
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray* attributes = [NSMutableArray array];
    for (NSInteger i=0 ; i < self.cellCount; i++) {
        NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
    }
    return attributes;
}

compositional layout添加header/footer supplementary items:

  let headerSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1.0),
    heightDimension: .estimated(44))
  let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
    layoutSize: headerSize,
    elementKind: AlbumsViewController.sectionHeaderElementKind, 
    alignment: .top)

  let section = NSCollectionLayoutSection(group: group)
  section.boundarySupplementaryItems = [sectionHeader]

UICompositional Layout

  • 自然滚动方向是横向,如果要纵向,需要设置UICollectionViewCompositionalLayoutConfiguration为横向,
    • 然后让一行显示一个元素,多余的元素自在横向的垂直方向orthogonal(纵向)上滚动起来了
  • scrollview的代理是应用在configuration.scrollIDirection上的
  • 垂直方向上的scroll代理是visibleItemsInvalidationHandler
    • visibleItems.lastObject就是下一个即将出来的元素(以满屏pager效果为例)
    • environment可以用.container得到其容器(就是collectionView)
  • 还没实验过,PagingSectionFooterView这是每个section一个footer吗?还是每个group

勘误1

上面做竖向列表的方法其实是没有理解透彻, 应该这么做: 设置grouWidth = 100%, itemWidth = 100%, 那么一个group就只有一个元素了, 而group是绝对的竖向滚动的(除非改动了configuration).

  1. 也就是说自然让group滚动就行了(group是虚拟的,没有对应的视图, 所以表现上还是视图的垂直列表)
  2. 原理是, item在group设定的方向上, 如果摆不下了, 就会摆到下一个group
  3. 如果设置了orthogonalScrollingBehavior, 不管是什么行为, 一个最大的作用是, 让第2条失效, 超出的元素不再摆到下一个group, 而是摆到后面, 滚动起来 对比下3设不设的区别, 假设有一个section, 15个items:
  4. 那就是一个竖向列表, 但是是15个group
  5. 设置了后, 变成了一个横向列表, 只有一个group了

勘误2

上述结论错了! 似乎是orthogonalScrollingBehavior设置了后group的滚动方向就变了, 变成了横向的, 而不是竖向的

证明:

  • itemWidth = 0.3groupWidth, itemheight = 0.5groupHeight
  • group竖向排列元素
  • 结果: 一页显示两个元素, 只有1/3页面宽, 但是从右边拖拽能看到下一页又有两个元素, 如此往复

证明了下一页是下一个group, 而如果如我之前所判断的, 应该一页就显示完了, 而且只能看见两个元素, 剩下的元素往上拖拽就能看到.

所以, 用compositional layout, 元素是没有"折行"一说的. 一折行, 就到了下一个group了(也没有隐藏, 滚动等说法)

group的构造方法有一个items版本的, 还有一个item:count版本的, 有什么区别?

  • items版本就是自然布局, group的范围摆不下了, 就自动摆到下一个group里, 不会"隐藏,折行,滚动"
  • count版本则会保证一个group里一定会摆进count个数目的item, 第n+1个才摆到下一个group
    • 如果group空间不够, 会呈现出元素只显示出一部分的效果
    • 但其实元素的size并没有变, 而是它的下一个元素的位置变了, 把前一个元素挡住了一部分, 这样就呈现出一个group只能呈现一部分元素的效果
    • 如果你用orthogonalScrollingBehavior让group滚动起来, 就会发现之前被挡住的元素显示出来了, 因为元素本身大小没变, 而挡它的"下一个"元素因为不再轴向排列, 而是交叉向排列, 就挡不住它了

group是什么

  • group是compositional layout里一个虚拟的概念, 它不是collectionView里的一个视图, 而是虚拟的, 用来描述collectionView里的元素
  • 它有width和height, 用来描述元素在group里的位置, 以及元素在group里的尺寸
  • 它有direction, 用来描述元素在group里的排列方向
  • 它有item, 用来描述元素在group里的排列方式
  • 它有itemSpacing, 用来描述元素在group里的间距
  • 它有sectionInset, 用来描述元素在group里的间距

上面几条是AI是自动生成的, 可以了解下, 但不是我要说的重点. 重点是, 它是collectionView计算每个位置的依据, 即哪些元素在group的哪个位置, 这样比写方法计算每个元素在全局的位置要高效一些. 而group里的itemCount, 则是限制了当你的item位置经过计算, 已经超出了group的空间, 是需要继续摆下去, 还是新生成一个group来重新计算位置

orthogonalScrollingBehavior改变了什么

  • 上面说了, 它设置的是交叉向的滚动行为, 但客观上等于是个开关, 即命令group按交叉向排列而不是按轴向排列.
  • 一旦设置了这个值, 所有的item就不是在collectionView内布局, 而是在collectionView下的_UICollectionViewOrthogonalScrollView里布局了, 它就是一个scrollView. 而且所有元素位置(经过group的辅助)计算直接映射到scrool上, 没有任何区别
    • 还是有区别的, 因为没有滚动的话, 是按轴向排列的, 加上滚动后, 就按交叉向排列来计算了.但是计算依据是一样的,不会考虑父容器是collectionView还是scrollView

勘误3

上面说了group里不折行, 错了. 那是使用的示例里元素高度与group高度相同(或者说没有可折行的足够行高)导致的. 以下是几种场景下的折行策略(以group纵向排列为例):

  1. 普通items接口下
    • 元素高度与group高度相同, 不折行
    • 元素高度小于group高度, 如果下一行能摆得下, 折行
    • 如果下一行高度不够, 不折行
    • 也就是说所谓的"元素高度与group高度相同", 不过就是"下一行的高度不够"的一个特例而已
    • 但是如果你用的是estimate的高度的话, 它总是有足够的高度来支撑下一行的, 因此总是可以折行
  2. item:count接口下
    • 不折行, 不管你设了几个count, 在轴向排满后后面的就显示不出来的, 显示了一部分的就下截断显示
    • 你让group横向滚动起来, 也是一样, 只不过是第二个group跟在了第一个group后面, 每个group都是显示不全的状态, 不会出现把第一个group显示完了才跟上第二个group的情况

既然group会折行, 那么行间距怎么设置? 比较下面两个方法:

item.contentInsets = .init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
group.interItemSpacing = .fixed(10.0)

只有方法一可行, 因为它控制每个元素, 方法二只会为左右加边距, 折行后的元素是不加的行间距的

神奇的事情发生了,

group.edgeSpacing = .init(leading: .fixed(0), top: .fixed(2.0), trailing: .fixed(0), bottom: .fixed(0))

这个属性为group内的元素增加了排版时增位移的能力, 用上了我上面那句话后, 行间距也有了(2px), 但是, 它还有leadingtrailing不是么? 我测了下, 无效, 也就是说, 注释掉了interItemSpacing后, 即使这里设了左右空间, 元素还是连在一起的.

总结:

  • 引入了group来辅助定位collectionView里的元素, 即只参与计算, 不参与生成视图
  • 引入了orthogonalScrollingBehavior来改变group的排列方向(从轴向改成交叉向)
  • itemsitem:count两种接口来描述元素在group里的排列方式(排满就走, 还是排满了也继续摆, 即使不可见)
  • 引入了itemSpacingsectionInset来描述元素在group里的间距
  • 引入了visibleItemsInvalidationHandler来作为collectionView里这个顶级scrollView的滚动事件代理, 还送入了section和environment等参数供使用
    • 不然你没有别的途径去跟踪滚动位置

其它

  • layoutEnvironment.container.effectiveContentSize为布局并应用各种space后容器的大小

以下显示给section footer加一个页面指示器

collectionView.register(PagingSectionFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: PagingSectionFooterView.reuseIdentifier)

let footerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(20))

let pagingFooterElement = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize, elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom)
section.boundarySupplementaryItems += [pagingFooterElement]

section.visibleItemsInvalidationHandler = { [weak self] (items, offset, env) -> Void in
    guard let self = self else { return }

    let page = round(offset.x / self.view.bounds.width)

    self.pagingInfoSubject.send(PagingInfo(sectionIndex: sectionIndex, currentPage: Int(page)))
}


Backlinks