本文迁移自简书 2018.05.25 18:14:07 著
老习惯先上效果图(和上篇文章一样):
# 本篇文章中,您将使用到:
-
UICollectionViewLayoutAttributes子类化,及相关的注意点; -
在
collectionViewCell中使用自定义的layoutAttributes来布局cell; -
简单的自定义
UICollectionViewLayout;
# 整体思路:
-
如上篇文章中提到的,
cell上布局了一个imageView,通过UICollectionViewLayoutAttributes的子类PageLayoutAttributes的contentOffsetX来更新iamgeView的位置; -
通过重写
UICollectionViewFlowLayout的shouldInvalidateLayoutForBoundsChange:方法来触发滚动collectionView时更新layout中的PageLayoutAttributes的contentOffsetX属性; -
PageLayoutAttributes通过isEqual:判断是否需要更新PageLayoutAttributes实例对应的cell; -
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 theisEqual: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
NSCopyingprotocol. It is very important that we also conform to this protocol and implementcopyWithZone:. 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 原因的时间花了一下午😳,因此在这里建议自定义 UICollectionViewLayoutAttributes 时在 isEqual ,仅有两种返回值:
return NO;;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