自定义UICollectionViewLayout|青训营笔记

321 阅读6分钟

自定义UICollectionViewLayout|青训营笔记

这是我参与「第四届青训营 」笔记创作活动的第3天

想看基础的,直接跳到基础

想看怎么自定义的,可以直接跳到实现

需求

我们组选择了华容道项目,容道布局成了难点,本次我们选择了UICollectionViewLayout

每一个人其实都是一样,那么就是一个UICollectionViewCell

我每一个人都有x, y, width, height,是可以根据这个来布局的

对于手势,我们可以发现苹果官方推荐我们使用其管理者持有,也就是UICollectionView

之后我会上线一个个人笔记,有关于学校项目里面的课表需求,也是使用UICollectionViewLayout

(课表这个需求很离谱,我会单独逐步分析出来,如果有人有类似需求,可以去我们仓库看,学校的项目是开源的,已上架App Store)

问题

为什么不用UIView

有人好奇,为什么一定要用这么重量级的东西,而且还要自定义UICollectionViewLayout

其实容道里面的人数不是固定的,如果利用NSMutableArray <__kindof UIView *> * 方式的话,还需要自己去维护这个数组,更加麻烦

手势一共4个方向,4个手势,如果以上面的方式的话,我不得不再去写一个方法判断手势是否在UIView里面

如果这里还有人想问为什么里面的单个UIView不加手势的话,可以考虑先查看一下设计模式与设计理念

IGListKit

有人建议我可以考虑这个第三方库,但我拒绝了,这个项目不会用到

但上面提到的课表中,我可能会使用到这个第三方库

基础

UICollectionView

这个视图,在很多人眼里都很重量级,原因就在他比UITableView还多一个UICollectionViewLayout

collectionView通过layout进行布局,这种布局相对来说比较更个性化。

一般来说,不需要继承这个类(除非要求手势穿透)

这里简单上一个懒加载的代码

- (UICollectionView *)collectionView {
    if (_collectionView == nil) {
        CGFloat width = self.view.width - 40;
        
        LevelCollectionLayout *layout = [[LevelCollectionLayout alloc] init];
        layout.lineSpacing = layout.interitemSpacing = 5;
        CGFloat minWidth = width / 4 - layout.interitemSpacing;
        layout.sizeForItem = CGSizeMake(minWidth, minWidth);
        
        _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, width - layout.interitemSpacing, (minWidth + layout.lineSpacing) * 5 - layout.lineSpacing) collectionViewLayout:layout];
        _collectionView.center = self.view.SuperCenter;
        _collectionView.backgroundColor = UIColor.clearColor;
        
        [_collectionView registerClass:PersonItem.class forCellWithReuseIdentifier:PersonItemReuseIdentifier];
        _collectionView.showsVerticalScrollIndicator = NO;
        _collectionView.showsHorizontalScrollIndicator = NO;
    }
    return _collectionView;
}

这里我们可以发现,我们先得搞到一个layout(后面会说到如何自定义)

然后根据位置和这个layout去创建

注册一个复用的cell(当然这个项目基本不复用,课表需要复用)

UICollectionViewCell

这个就当非常普通的UIView也可以,如果是在复用池的Cell被再次使用,回掉用

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes

你就可以根据传来的UICollectionViewLayoutAttributes来变化当前视图布局

(纯Frame布局非常好用)

UICollectionViewLayout

我们必须指定子类,至于为什么,实现的时候就知道了。

这里官方给出了UICollectionViewFlowLayout

这是一种线性布局,什么是线性呢?

  • 下一个indexPath的数据在视觉上紧接上一个
  • 下一个视图在视觉上紧接上一个

而华容道不同,下一个人的视觉位置可能并不会紧接上一个

而且,在滑动后,更能看出这个需求并不能用UICollectionViewFlowLayout实现

实现

代理

UICollectionViewFlowLayout其实也有代理,不过他的代理实现机制是靠collectionView本身的delegete去回传。

这里可以和UICollectionViewFlowLayout一样的思路,继承UICollectionViewDelegate书写协议,当然也可以不继承。

这里只会讲解不继承,在课表需求中,我们会讲解继承协议,以及如何打断UICollectionView的delegate。

@protocol LevelCollectionLayoutDelegate <NSObject>

@required

/// 确定布局
/// @param collectionView 视图
/// @param layout 布局
/// @param indexPath 位置
- (PersonFrame)collectionView:(UICollectionView *)collectionView layout:(LevelCollectionLayout *)layout frameForItemAtIndexPath:(NSIndexPath *)indexPath;

@end
  
/// typedef struct _PersonFrame {
///     int x;
///     int y;
///     int width;
///     int height;
/// } PersonFrame;

因为这个需求并没有其他要求,这里只有一个required代理,用于确定一个人的位置。

属性

与UICollectionViewFlowLayout一样,你也可以规定一些属性,

不管是计算属性还是存储属性,这些属性将会在后期进行大规模运算。

当然,这里少不了的属性一定是代理,这里不上代码。

类扩展

在刚刚的Cell中,UICollectionViewLayoutAttributes这个名字超长的类突然有点难让人理解。

简单来说,这里面规定了Cell的所有视觉空间布局信息

就是说,这里面有大小,3D,transform等等对这个Cell空间视觉上的数据支持

这里非常重要的一点是,在使用CollectionView的- performBatchUpdates:方法时,

去改变UICollectionViewLayoutAttributes的值是非常有效的(至少挺好看)

@interface LevelCollectionLayout ()

/// 布局
@property (nonatomic, strong) NSMutableArray <UICollectionViewLayoutAttributes *> *attributes;

@end

所以在很多资料中,都会有这么一个东西存在,就非常好理解了,你有n个数据,那么你就有n个视觉空间布局信息。

复写

我们必须复写一些信息,才能让Layout能执行起来,那么之前也提到了为什么一定要子类呢?

原因就在**UICollectionViewLayout (UISubclassingHooks)**里面的

- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect方法,这个方法必然会掉用,而如果不实现,那么就得不到UICollectionViewLayoutAttributes,就会报错了。

- (NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    return self.attributes;
}

而在一般的项目中,这里就会返回类扩展的那个attributes。

如果项目比较复杂,比如想分section去管理attributes等。

赋值

看了半天,结果发现attributes没值,怎么办呢?

重写- prepareLayout或者- invalidateLayout。这个项目呢,我就选择了prepareLayout。

不过要注意的一点是一定要掉用super

- (void)prepareLayout {
    [super prepareLayout];
    
  	// 赋值操作
}

赋值对应的是创建,这个根据需求来,比如华容道当前关卡是不会平白无故新增一个人,那就可以用懒加载

如果对于课表来说,我新增一个事务课表,那么这个维护就得靠Layout本身的CRUD了。

那怎么赋值呢,这里我就单独抽出来一个方法吧

- (CGRect)_frameForIndexPath:(NSIndexPath *)indexPath {
    if (self.delegate) {
        PersonFrame frame = [self.delegate collectionView:self.collectionView layout:self frameForItemAtIndexPath:indexPath];
        CGFloat x = frame.x * (self.interitemSpacing + self.sizeForItem.width);
        CGFloat y = frame.y * (self.lineSpacing + self.sizeForItem.height);
        CGFloat width = frame.width * self.sizeForItem.width + (frame.width - 1) * self.interitemSpacing;
        CGFloat height = frame.height * self.sizeForItem.height + (frame.height - 1) * self.lineSpacing;
        return CGRectMake(x, y, width, height);
    }
    return CGRectZero;
}

// attribute.frame = [self _frameForIndexPath:indexPath];

很简单吧?因为attribute本身可以理解为只是一个存储类,存储这些空间布局信息,仅此而已。

总结

好了,这篇文章有点长了,其他的方法都大同小异,需要的时候再去看就好了。

关于如何通过自定义UICollectionViewLayout改变布局,也就是棋子如何走呢,就放到下一个笔记吧。

如果对CollectionView感兴趣的话,可以期待一下讲解课表需求。