UICollectionView实现婚礼纪欢迎页

1,493 阅读4分钟

本文迁移自简书 2018.05.25 18:14:07 著

老习惯先上效果图(和上篇文章一样):

collectionViewForWecomePage.gif

# 本篇文章中,您将使用到:

  1. UICollectionViewLayoutAttributes 子类化,及相关的注意点;

  2. collectionViewCell 中使用自定义的 layoutAttributes 来布局 cell ;

  3. 简单的自定义 UICollectionViewLayout;

# 整体思路:

  1. 如上篇文章中提到的, cell 上布局了一个 imageView ,通过 UICollectionViewLayoutAttributes 的子类 PageLayoutAttributescontentOffsetX 来更新 iamgeView 的位置;

  2. 通过重写 UICollectionViewFlowLayoutshouldInvalidateLayoutForBoundsChange: 方法来触发滚动 collectionView 时更新layout中的 PageLayoutAttributescontentOffsetX 属性;

  3. PageLayoutAttributes 通过 isEqual: 判断是否需要更新 PageLayoutAttributes 实例对应的 cell ;

  4. cell 通过 applyLayoutAttributes: 获取布局属性,进行布局;

# UICollectionViewLayoutAttributes子类化

官方文档中的注意点如下:

If you subclass and implement any custom layout attributes, you must also override the inherited isEqual: method to compare the values of your properties. In iOS 7 and later, the collection view does not apply layout attributes if those attributes have not changed. It determines whether the attributes have changed by comparing the old and new attribute objects using the isEqual: method. Because the default implementation of this method checks only the existing properties of this class, you must implement your own version of the method to compare any additional properties. If your custom properties are all equal, call super and return the resulting value at the end of your implementation.

如果继承了 UICollectionViewLayoutAttributes 并且添加了任何自定义的 layout attributes ,也必须实现 isEqual: 方法来比较自定义属性. 在 iOS7 (包括 iOS7 )以后,如果 UICollectionViewLayoutAttributes 的属性值没有改变, collection view 不会应用 layout attributes ,这些 layout attributes 的是否改变由 isEqual: 的返回值来决定. 在重写 isEqual: 时, 除了需要处理自定义属性外, 还需要注意父类方法的调用.

Because layout attribute objects may be copied by the collection view, it conforms to the NSCopying protocol. It is very important that we also conform to this protocol and implement copyWithZone:. Otherwise, our property will always be zero (as guaranteed by the compiler).

由于 layout attributes 对象可能会被 collection view 复制 , 因此 layout attributes 对象应该遵循 NSCoping 协议,并实现 copyWithZone: 方法, 否则我们获取的自定义属性会一直是空值.

举例如下:

/** subclass must conforms to the NSCopying protocol */
- (id)copyWithZone:(NSZone *)zone {

    CLSectionColorLayoutAttributes *layoutAttributes = [super copyWithZone:zone];
    layoutAttributes.sectionColor = self.sectionColor;
    return layoutAttributes;
}


/** In iOS 7 and later, the collection view does not apply layout attributes if
 those attributes have not changed. It determines whether the attributes have changed
 by comparing the old and new attribute objects using the isEqual: method. */
- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }
    if ([object class] == [self class]) {
        return [super isEqual:object] && (self.sectionColor == [object sectionColor]);
    }
    return NO;
}

这里说下本文 demo 中的碰到的实际情况:

首先,我们在自定义的 PageLayoutAttributes 增添了 contentOffsetX 属性来控制图片的偏移量:

@interface PageLayoutAttributes : UICollectionViewLayoutAttributes

/**** 偏移量 ***/
@property (nonatomic, assign) CGFloat contentOffsetX;

@end

然后在 .m 文件中实现了 copyWithZone:isEqual: 方法


@implementation PageLayoutAttributes

- (BOOL)isEqual:(id)object {
    /*
     //这里的判断永远是不相等的(仅本例)
    if (self == object) {
        return YES;
    }
     */
    if ([object isKindOfClass:[PageLayoutAttributes class]]) {
        PageLayoutAttributes *newObject = (PageLayoutAttributes *)object;
        if (newObject.contentOffsetX == self.contentOffsetX) {
        	  //BUG点
            return YES;
        }
        return [super isEqual:object];
    }
    return [super isEqual:object];
}

- (instancetype)copyWithZone:(NSZone *)zone {
    PageLayoutAttributes *model = [super copyWithZone:zone];
    model.contentOffsetX = self.contentOffsetX;
    return model;
}

@end

这里 //BUG点 ,直接返回了 YES , 结果布局出来的视图呈现了如下效果:

BUG版本.gif

找这个 BUG 原因的时间花了一下午😳,因此在这里建议自定义 UICollectionViewLayoutAttributes 时在 isEqual ,仅有两种返回值:

  1. return NO; ;
  2. return [super isEqual:object]; ;

修改后的代码请参阅demo

# 自定义的UICollectionViewLayout

本demo中对 layout 的需求过于简单,重点在 layout 中使用子类化的 layoutAttributes ,因此demo中通过继承 UICollectionViewFlowLayout 来实现;

#import "WecomePageFlowLayout.h"
#import "PageLayoutAttributes.h"

@interface WecomePageFlowLayout ()

/**** cell的总数 ***/
@property (nonatomic, assign) NSInteger cellCount;

@property (nonatomic, copy) NSArray *attributsArray;

@end

@implementation WecomePageFlowLayout


- (void)prepareLayout {
    [super prepareLayout];
    _cellCount = [[self collectionView] numberOfItemsInSection:0];
}
//告诉 layout 使用 自定义的 attributes 来布局
+ (Class)layoutAttributesClass {
    return [PageLayoutAttributes class];
}


/*!
 *  多次调用 只要滑出范围就会 调用
 *  当CollectionView的显示范围发生改变的时候,是否重新发生布局
 *  一旦重新刷新 布局,就会重新调用
 *  1.layoutAttributesForElementsInRect:方法
 *  2.preparelayout方法
 */
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    //计算当前的偏移量
    CGFloat width = CGRectGetWidth(self.collectionView.bounds);
    CGFloat offsetX = self.collectionView.contentOffset.x;
    NSInteger index = offsetX / width;
    PageLayoutAttributes *curretnAttribute = self.attributsArray[index];
    PageLayoutAttributes *nextAttribute = nil;
    if (index < _cellCount -1) {
        nextAttribute = self.attributsArray[index + 1];
    }
    //当前的item对应的attribute设置偏移量为0
    curretnAttribute.contentOffsetX = 0;
    if (nextAttribute) {
        //正在出现的item对应的attribute设置偏移量为跟随collectionView的offset动态计算
        nextAttribute.contentOffsetX = -(width * 0.5 - (offsetX - width * index) * 0.5);
    }
    return self.attributsArray;
}


#pragma mark -- tools

- (NSArray *)attributsArray {
    if (!_attributsArray) {
        NSMutableArray *array = [NSMutableArray array];
        CGFloat width = CGRectGetWidth([UIScreen mainScreen].bounds);
        CGFloat height = CGRectGetHeight([UIScreen mainScreen].bounds);
        for (NSInteger i = 0; i < _cellCount; i++) {
            NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
            PageLayoutAttributes *attribute = [PageLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
            attribute.contentOffsetX = (i == 0 ? 0 : -(CGRectGetWidth([UIScreen mainScreen].bounds) * 0.5));
            attribute.frame = CGRectMake(i * width, 0, width, height);
            [array addObject:attribute];
        }
        self.attributsArray = [array copy];
    }
    return _attributsArray;
}

@end

这里对于比较复杂的 layout 需求给出一个苹果官方例子作为参考: CircleLayout ;

# 在collectionViewCell中使用子类化layoutAttributes布局

使用 - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes; 方法来进行自定义布局

#import "WecomePageCell.h"
#import "PageLayoutAttributes.h"

@interface WecomePageCell ()

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *offsetX;
@property (weak, nonatomic) IBOutlet UIImageView *cImageView;

@end

@implementation WecomePageCell

- (void)awakeFromNib {
    [super awakeFromNib];
}

//使用LayoutAttributes布局Cell
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    [super applyLayoutAttributes:layoutAttributes];
    if ([layoutAttributes isKindOfClass:[PageLayoutAttributes class]]) {
        self.offsetX.constant = [(PageLayoutAttributes *)layoutAttributes contentOffsetX];
    }
}

#pragma mark === public
- (void)updateImage:(UIImage *)image {
    [self.cImageView setImage:image];
}

@end

# 结束

附上:demo