UICollectionView自定义布局

本文详细介绍了如何自定义UICollectionView布局,包括理解布局过程、核心任务、生成布局属性、使用附属视图和装饰视图等,并提供了一个具体的AWCollectionViewDialLayout实例代码,展示了自定义布局的实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继承UICollectionViewLayout
     需要实现的核心任务:
1.指定滚动区域的尺寸
2.为布局中的每一个cell或view指定attribute对象,从而使collection view得到item的位置。
     在布局过程中,使用collectionView属性从数据源获取数据。
理解核心布局过程
     集合视图和布局对象协同工作,只要集合视图需要布局信息,就向布局对象询问。集合视图第一次出现,或者尺寸改变都会获取布局信息,可以显式调用invalidateLayout方法强制重新布局。
     invalidateLayout与reloadData区别很大,重新布局不会刷新数据。在数据源中的数据变化时,reloadData才是合适的。
在布局过程中,集合视图调用了布局对象中的几个特定方法:
     1.在prepareLayout方法中完成布局信息的预处理。
     2.在collectionViewContentSize中基于预处理完成全部计算。
     3.在layoutAttributesForElementsInRect:中返回设定好的attribute。
     invalidateLayout方法并不会马上开始布局更新,仅仅标记了布局需要更新,在下一次视图更新循环中,集合视图发现布局需要更新时,才会更新布局。
生成布局属性
     布局属性是UICollectionViewLayoutAttributes类的对象,使用以下方法生成布局属性:
1.layoutAttributesForCellWithIndexPath:
2.layoutAttributesForSupplementaryViewOfKind:withIndexPath:
3.layoutAttributesForDecorationViewOfKind:withIndexPath:
     至少为布局属性设置尺寸和位置,zIndex用于防止重叠,isEqual:方法需要重写,集合视图调用用于某些计算。
准备布局
     在这完成准备工作。
为指定区域中的item设置attribute
     layoutAttributesForElementsInRect:方法的步骤:
1.遍历prepareLayout方法中准备的数据,填入缓存的attribute,没有就生成一个;
2.检查每个item的尺寸,看看在不在要布局的矩形区域内;
3.对于在区域内的item,添加一个对应的attribute对象到数组中
4.返回attribute对象数组
需要立刻返回布局属性的情况
     layoutAttributesForItemAtIndexPath:
     layoutAttributesForSupplementaryViewOfKind:atIndexPath:
     layoutAttributesForDecorationViewOfKind:atIndexPath:
     这些方法的实现需要取到指定item的当前布局属性并返回。每个自定义布局对象都重写实现第一个方法,如果没有supplementary view可以不重写第二个方法,如果没有decoration view可以不重写第三个方法,在这几个方法中不能更新布局属性。
让自定义布局更加迷人
     有一些方法推荐实现,效果更好。
使用附属视图为内容增色
     附属视图与cell是分离开的,有另一套布局属性。附属视图的数据来自数据源,用于说明主内容。流布局使用附属视图作为区头区尾,有的应用使用附属视图为每一个cell提供了一个说明文本。附属视图应该是UICollectionReusableView的子类。
     为布局添加附属视图的步骤:
1.在集合视图中注册附属视图,registerClass:forSupplementaryViewOfKind:withReusableIdentifier:或者
registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
2.在数据源中实现collectionView:viewForSupplementaryElementOfKind:atIndexPath:方法,使用dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:重用或新建附属视图,设置后返回
3.像为cell设置布局属性一样为附属视图设置布局属性
4.在layoutAttributesForElementsInRect:方法的返回数组中加入附属视图的布局属性
5.实现layoutAttributesForSupplementaryViewOfKind:atIndexPath:方法,在需要时返回特定附属的布局属性
     附属视图的布局过程与cell一样,但是附属视图可以有多种附属视图样式。因为不同的cell内容可能需要有不同的附属视图来配合,附属视图有一个额外的kind字段描述。
在自定义布局中加入装饰视图
     装饰视图是用于改善集合视图外观的视图,只有外观所以不需要数据源。可以使用装饰视图实现cell的个性化。
     添加装饰视图的步骤:
1.使用registerClass:forDecorationViewOfKind:或者registerNib:forDecorationViewOfKind:注册装饰视图。
2.在布局对象的layoutAttributesForDecorationViewOfKind:atIndexPath:方法中为装饰视图设置布局属性。
3.实现layoutAttributesForDecorationViewOfKind:atIndexPath:,并在需要时返回布局属性。
4.可选实现,initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:和finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:用于处理装饰视图的动态出现、消失效果。
     所有的配置都在initWithFrame:中完成,必须是UICollectionReusableView的子类。为装饰视图设置布局属性时,需要考虑zIndex,防止遮盖。
实现自定义布局的技巧
1.在prepareLayout方法中创建、保存稍后要用的UICollectionViewLayoutAttributes。在item少的时候缓存有效,多的时候就得考虑了。
2.不要继承UICollectionView。
3.绝不要在layoutAttributesForElementsInRect:方法中使用UICollectionView的visibleCells属性,会产生循环。有时候,布局对象需要通过数据源定位item。
其中一个实例的具体的代码如下:
#import "AWCollectionViewDialLayout.h"
@implementation AWCollectionViewDialLayout
- (id)init
{
    if ((self = [super init]) != NULL)
    {
[self setup];
    }
    return self;
}
-(id)initWithRadius: (CGFloat) radius andAngularSpacing: (CGFloat) spacing andCellSize: (CGSize) cell andAlignment:(WheelAlignmentType)alignment andItemHeight:(CGFloat)height andXOffset: (CGFloat) xOff{
    if ((self = [super init]) != NULL)
    {
        self.dialRadius = radius;//420.0f;
        self.cellSize = cell;//(CGSize){ 220.0f, 80.0f };
        self.itemHeight = height;
        self.AngularSpacing = spacing;//8.0f;
        self.xOffset = xOff;
        self.wheelType = alignment;
        [self setup];
    }
    return self;
}
- (void)setup
{
    self.offset = 0.0f;    
}
- (void)prepareLayout
{
    [super prepareLayout];
    self.cellCount = (int)[self.collectionView numberOfItemsInSection:0];
    self.offset = -self.collectionView.contentOffset.y / self.itemHeight;
    
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray *theLayoutAttributes = [[NSMutableArray alloc] init];
    
    float minY = CGRectGetMinY(rect);
    float maxY = CGRectGetMaxY(rect);
    
    int firstIndex = floorf(minY / self.itemHeight);
    int lastIndex = floorf(maxY / self.itemHeight);
    int activeIndex = (int)(firstIndex + lastIndex)/2;
    int maxVisibleOnScreen = 180 / self.AngularSpacing + 2;
    
    int firstItem = fmax(0, activeIndex - (int)(maxVisibleOnScreen/2) );
    int lastItem = fmin( self.cellCount-1 , activeIndex + (int)(maxVisibleOnScreen/2) );
    //float firstItem = fmax(0 , floorf(minY / self.itemHeight) - (90/self.AngularSpacing) );
    //float lastItem = fmin( self.cellCount-1 , floorf(maxY / self.itemHeight) );
    for( int i = firstItem; i <= lastItem; i++ ){
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *theAttributes = [self layoutAttributesForItemAtIndexPath:indexPath];
        [theLayoutAttributes addObject:theAttributes];
    }
    return [theLayoutAttributes copy];
}
- (CGSize)collectionViewContentSize
{
    const CGSize theSize = {
        .width = self.collectionView.bounds.size.width,
        .height = (self.cellCount-1) * self.itemHeight + self.collectionView.bounds.size.height,
    };
    return(theSize);
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    double newIndex = (indexPath.item + self.offset);
    
    UICollectionViewLayoutAttributes *theAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    theAttributes.size = self.cellSize;
    float scaleFactor;
    float deltaX;
    CGAffineTransform translationT;
    CGAffineTransform rotationT = CGAffineTransformMakeRotation(self.AngularSpacing* newIndex *M_PI/180);
    if(indexPath.item == 3){
//        NSLog(@"angle 3 :%f", self.AngularSpacing* newIndex);
    }
    
    
    if( self.wheelType == WHEELALIGNMENTLEFT){
        scaleFactor = fmax(0.6, 1 - fabs( newIndex *0.25));
        deltaX = self.cellSize.width/2;
        theAttributes.center = CGPointMake(-self.dialRadius + self.xOffset  , self.collectionView.bounds.size.height/2 + self.collectionView.contentOffset.y);
        translationT =CGAffineTransformMakeTranslation(self.dialRadius + (deltaX*scaleFactor) , 0);
    }else  {
        scaleFactor = fmax(0.4, 1 - fabs( newIndex *0.50));
        deltaX =  self.collectionView.bounds.size.width/2;
        theAttributes.center = CGPointMake(-self.dialRadius + self.xOffset , self.collectionView.bounds.size.height/2 + self.collectionView.contentOffset.y);
        translationT =CGAffineTransformMakeTranslation(self.dialRadius  + ((1 - scaleFactor) * -30) , 0);
    }
    CGAffineTransform scaleT = CGAffineTransformMakeScale(scaleFactor, scaleFactor);
    theAttributes.alpha = scaleFactor;    
    theAttributes.transform = CGAffineTransformConcat(scaleT, CGAffineTransformConcat(translationT, rotationT));
    theAttributes.zIndex = indexPath.item;
    
    return(theAttributes);
}


@end


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值