UICollectionView
首先从collectionView说起,collectionView由三个部分构成:
- Cells
- Supplementary Views 追加视图 (类似Header或者Footer)
- Decoration Views 装饰视图 (用作背景展示)
一方面,collectionView和tableview一样,由提供数据的UICollectionViewDataSource以及处理用户交互的UICollectionViewDelegate支持。另一方面,对于cell的样式和组织方式,由于collectionView比tableView要复杂得多,collectionView使用一个类UICollectionViewLayout对布局和行为进行描述。UICollectionViewLayout也是collectionView的精髓所在。
UICollectionViewLayoutAttributes
UICollectionViewLayoutAttributes是一个非常重要的类,先来看看property列表:
@property (nonatomic) CGRect frame
@property (nonatomic) CGPoint center
@property (nonatomic) CGSize size
@property (nonatomic) CATransform3D transform3D
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
@property (nonatomic, getter=isHidden) BOOL hidden
可以看到,UICollectionViewLayoutAttributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和DataSource的行为十分类似,当UICollectionView在获取布局时将针对每一个indexPath的部件(包括cell,追加视图和装饰视图),向其上的UICollectionViewLayout实例询问该部件的布局信息(在这个层面上说的话,实现一个UICollectionViewLayout的时候,其实很像是zap一个delegate,之后的例子中会很明显地看出),这个布局信息,就以UICollectionViewLayoutAttributes的实例的方式给出。
UICollectionViewLayout
UICollectionViewLayout的功能为向UICollectionView提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layout的常规做法是继承UICollectionViewLayout类,然后重载下列方法:
-(CGSize)collectionViewContentSize
返回collectionView的内容的尺寸
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
返回rect中的所有的元素的布局属性
返回的是包含UICollectionViewLayoutAttributes的NSArray
UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的UICollectionViewLayoutAttributes:
layoutAttributesForCellWithIndexPath:
layoutAttributesForSupplementaryViewOfKind:withIndexPath:
layoutAttributesForDecorationViewOfKind:withIndexPath:
-(UICollectionViewLayoutAttributes _)layoutAttributesForItemAtIndexPath:(NSIndexPath _)indexPath
返回对应于indexPath的位置的cell的布局属性
-(UICollectionViewLayoutAttributes _)layoutAttributesForSupplementaryViewOfKind:(NSString _)kind atIndexPath:(NSIndexPath *)indexPath
返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
-(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString_)decorationViewKind atIndexPath:(NSIndexPath _)indexPath
返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
另外需要了解的是,在初始化一个UICollectionViewLayout实例后,会有一系列准备方法被自动调用,以保证layout实例的正确。
首先,-(void)prepareLayout
将被调用,默认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。
之后,-(CGSize) collectionViewContentSize
将被调用,以确定collection应该占据的尺寸。注意这里的尺寸不是指可视部分的尺寸,而应该是所有内容所占的尺寸。collectionView的本质是一个scrollView,因此需要这个尺寸来配置滚动行为。
接下来-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
被调用,这个没什么值得多说的。初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes
来决定。
另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize
和-layoutAttributesForElementsInRect
来生成更新后的布局。
通过自定义UICollectionViewLayout实现的效果:
部分代码:
**
* 第一个方法
*
* @return 可见区域的内容尺寸
*/
-(CGSize)collectionViewContentSize{
return [super collectionViewContentSize];
}
/**
* 重载第二个方法
*
* 返回rect中所有元素的布局属性
* UICollectionViewAttributes可以是cell、追加视图以及装饰信息,通过以下三个不同的方法可以获取到不同类型的UICollectionViewLayoutAttributes属性
* layoutAttributesForCellWithIndexPath: 返回对应cell的UICollectionViewAttributes布局属性
* layoutAtttibutesForSupplementaryViewOfKind:withIndexPath: 返回装饰的布局属性 如果没有追加视图可不重载
* layoutAttributesForDecorationViewOfKind:withIndexPath: 返回装饰的布局属性 如果没有可以不重载
*
* @return 包含UICollectionViewLayoutAttributes的NSArray
*/
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
//1、获取可见区域
CGRect visibleRect = CGRectMake(self.collectionView.contentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
//2、获得这个区域的item
NSArray *visibleItemArray = [super layoutAttributesForElementsInRect:visibleRect];
//3、遍历,让靠近中心线的的item方法,离开的缩小
for (UICollectionViewLayoutAttributes *attributes in visibleItemArray) {
//1、获取每个item距离可见区域左侧边框的距离 有正负
CGFloat leftMargin = attributes.center.x - self.collectionView.contentOffset.x;
//2、获取边框距离屏幕中心的距离(固定的)
CGFloat halfCenterX = self.collectionView.frame.size.width / 2;
//3、获取距离中心的偏移量,需要绝对值
CGFloat absOffset = fabs(halfCenterX - leftMargin);
//4、获取的实际的缩放比例,距离中心越多,这个值就越小,也就是item的scale越小。中心是最大的。
CGFloat scale = 1 - absOffset / halfCenterX;
//5、缩放
attributes.transform3D = CATransform3DMakeScale(1 + scale * XFMinZoomScale, 1 + scale * XFMinZoomScale, 1);
//是否需要透明
if (self.needAlpha) {
if (scale < 0.6) {
attributes.alpha = 0.6;
}else if (scale > 0.99){
attributes.alpha = 1.0;
}else{
attributes.alpha = scale;
}
}
}
NSArray *attributesArr = [[NSArray alloc]initWithArray:visibleItemArray copyItems:YES];
return attributesArr;
}
/**
* 第三个属性重载 滚动的时候一直调用
*
* @param newBounds 这里的newBounds变化的只有x值的变化,也就是偏移量的变化
*
* @return 当边界发生变化的时候,是否应该刷新布局。如果YES那么就是边界发生变化的时候,重新计算布局信息 这里的newBounds变化的只有x值的变化,也就是偏移量的变化
*/
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
//把collectionView本身的中心位置(固定的),转换成collectionView整个内容上的point
CGPoint pInView = [self.collectionView.superview convertPoint:self.collectionView.center toView:self.collectionView];
//通过坐标获取对应的indexpath
NSIndexPath *indexPathNow = [self.collectionView indexPathForItemAtPoint:pInView];
if (indexPathNow.row == 0) {
if (newBounds.origin.x < SCREEN_WIDTH / 2) {
if (_index != indexPathNow.row) {
_index = 0;
if (self.delegate && [self.delegate respondsToSelector:@selector(collectionViewScrollToIndex:)]) {
[self.delegate collectionViewScrollToIndex:_index];
}
}
}
}else{
if (_index != indexPathNow.row) {
_index = indexPathNow.row;
if (self.delegate && [self.delegate respondsToSelector:@selector(collectionViewScrollToIndex:)]) {
[self.delegate collectionViewScrollToIndex:_index];
}
}
}
[super shouldInvalidateLayoutForBoundsChange:newBounds];
return YES;
}
/**
* item自动中心对齐
*
* @param proposedContentOffset <#proposedContentOffset description#>
* @param velocity <#velocity description#>
*
* @return 让滚动的item根据距离中心的值,确定哪个必须展示在中心,不会像普通的那样滚动到哪里就停到哪里
*/
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity{
//ProposeContentOffset是本来应该停下的位置
//1、先给一个字段存储最小的偏移量,那么默认就是无限放大
CGFloat minOffset = CGFLOAT_MAX;
//2、获取到可见区域的centerX
CGFloat horizontalCenter = proposedContentOffset.x + self.collectionView.bounds.size.width / 2;
// 3. 拿到可见区域的rect
CGRect visibleRec = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
// 4. 获取到所有可见区域内的item数组
NSArray *visibleAttributes = [super layoutAttributesForElementsInRect:visibleRec];
// 遍历数组,找到距离中心最近偏移量是多少
for (UICollectionViewLayoutAttributes *atts in visibleAttributes)
{
// 可见区域内每个item对应的中心X坐标
CGFloat itemCenterX = atts.center.x;
// 比较是否有更小的,有的话赋值给minOffset
if (fabs(itemCenterX - horizontalCenter) <= fabs(minOffset)) {
minOffset = itemCenterX - horizontalCenter;
}
}
// 这里需要注意的是 上面获取到的minOffset有可能是负数,那么代表左边的item还没到中心,如果确定这种情况下左边的item是距离最近的,那么需要左边的item居中,意思就是collectionView的偏移量需要比原本更小才是,例如原先是1000的偏移,但是需要展示前一个item,所以需要1000减去某个偏移量,因此不需要更改偏移的正负
// 但是当propose小于0的时候或者大于contentSize(除掉左侧和右侧偏移以及单个cell宽度) 、
// 防止当第一个或者最后一个的时候不会有居中(偏移量超过了本身的宽度),直接卡在推荐的停留位置
CGFloat centerOffsetX = proposedContentOffset.x + minOffset;
if (centerOffsetX < 0) {
centerOffsetX = 0;
}
if (centerOffsetX > self.collectionView.contentSize.width -(self.sectionInset.left + self.sectionInset.right + self.itemSize.width)) {
centerOffsetX = floor(centerOffsetX);
}
return CGPointMake(centerOffsetX, proposedContentOffset.y);
}
demo下载:http://download.youkuaiyun.com/detail/qq_34195670/9606956
github下载地址:https://github.com/goingmyway1/UICollectionViewLayout
理论部分参考:https://onevcat.com/2012/08/advanced-collection-view/ 王巍 先生