自定义 UICollectionViewLayout实现横向布局分页Emoji

需求:emoji的横向显示,在每页的最后需要显示一个删除按钮,如下图所示。

prepareLayout

在collectionview显示或更新时总会先调用此方法,该方法的默认实现什么都不做。子类可以覆盖它,并使用它来设置数据结构或执行以后执行布局所需的任何初始计算。

清楚了流程,代码很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//  BIEmojiCollectionViewLayout.h
#import <UIKit/UIKit.h>

@interface BICollectionViewFixedSizeLayout : UICollectionViewLayout

@property (nonatomic) CGSize itemSize;//item的size
@property (nonatomic) NSUInteger numberOfLinesPerPage;//每页的行数
@property (nonatomic) UIEdgeInsets contentInsets;//列表的内边距
@property (nonatomic) CGFloat minimalColumnSpacing;//item之间的间隙
@property (nonatomic) BOOL anchorLastItemPerPage;//是否每页最后一项是特殊的item,也就是需求中的删除按钮

- (BOOL)isLastItemPerPageAtIndexPath:(NSIndexPath *)indexPath;//获取当前indexPath是否是特殊的item

@end


//BIEmojiCollectionViewLayout.m
#import "BICollectionViewFixedSizeLayout.h"

#import "BICollectionViewFixedSizeLayout.h"

@implementation BICollectionViewFixedSizeLayout {
NSMutableDictionary<NSIndexPath *,UICollectionViewLayoutAttributes *> *_layoutAttributes;
CGSize _contentSize;
NSUInteger _numberOfColumnsPerPage;
}

#pragma mark - life cycle methods

- (instancetype)init {
self = [super init];
if (self) {
[self commonInit];
}
return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonInit];
}
return self;
}

#pragma mark - public methods

- (BOOL)isLastItemPerPageAtIndexPath:(NSIndexPath *)indexPath {
if (self.anchorLastItemPerPage) {
if ((indexPath.row + 1) % (_numberOfColumnsPerPage * _numberOfLinesPerPage) == 0 ||
indexPath.row == [self.collectionView numberOfItemsInSection:0] - 1) {
return YES;
}
}
return NO;
}

#pragma mark - private methods

- (void)commonInit {
_itemSize = CGSizeMake(40, 40);
_numberOfLinesPerPage = 3;
_anchorLastItemPerPage = NO;
_minimalColumnSpacing = 1.0;
_contentInsets = UIEdgeInsetsMake(5, 5, 5, 5);
_layoutAttributes = [NSMutableDictionary dictionary];
}

#pragma mark - custom UICollectionViewLayout methods

- (void)prepareLayout {
[super prepareLayout];
//clean up
[_layoutAttributes removeAllObjects];
_contentSize = CGSizeZero;
//caculate attibutes
CGFloat collectionViewWidth = CGRectGetWidth(self.collectionView.bounds);
CGFloat collectionViewHeight = CGRectGetHeight(self.collectionView.bounds);

CGFloat itemWidth = _itemSize.width;
CGFloat itemHeight = _itemSize.height;
_numberOfColumnsPerPage = (collectionViewWidth - _contentInsets.left - _contentInsets.right + _minimalColumnSpacing) / (_itemSize.width + _minimalColumnSpacing);

NSAssert([self.collectionView numberOfSections] == 1, @"number of sections should equal to 1.");
NSUInteger numberOfItemsPerPage = _numberOfColumnsPerPage * _numberOfLinesPerPage;
CGFloat columnSpacing = _numberOfColumnsPerPage == 1 ? 0.0 : (collectionViewWidth - _contentInsets.left - _contentInsets.right - _numberOfColumnsPerPage * itemWidth) / (_numberOfColumnsPerPage - 1);
CGFloat lineSpacing = _numberOfLinesPerPage == 1 ? 0.0 : (collectionViewHeight - _contentInsets.top - _contentInsets.bottom - _numberOfLinesPerPage * itemHeight) / (_numberOfLinesPerPage - 1);
NSUInteger numberOfRows = [self.collectionView numberOfItemsInSection:0];
for (NSUInteger row = 0; row < numberOfRows; row++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
NSUInteger currentPage = floor(row / numberOfItemsPerPage);
NSUInteger currentItemOnPageIndex = (row % numberOfItemsPerPage);
//
CGFloat originX = 0.0;
CGFloat originY = 0.0;
if (_anchorLastItemPerPage && (row == numberOfRows - 1)) {
originX = (currentPage + 1) * collectionViewWidth - _contentInsets.right - itemWidth;
originY = collectionViewHeight - _contentInsets.bottom - itemHeight;
} else {
originX = _contentInsets.left + (row % _numberOfColumnsPerPage) * (columnSpacing + itemWidth) + currentPage * collectionViewWidth;
originY = _contentInsets.top + (currentItemOnPageIndex / _numberOfColumnsPerPage) * (lineSpacing + itemHeight);
}

// all attributes
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.size = CGSizeMake(itemWidth, itemHeight);
attributes.frame = CGRectMake(originX, originY, itemWidth, itemHeight);
_layoutAttributes[indexPath] = attributes;
}

// content size
NSUInteger pages = ceil(numberOfRows / (1.0 * numberOfItemsPerPage));
_contentSize = CGSizeMake(collectionViewWidth * pages, collectionViewHeight);
}

- (CGSize)collectionViewContentSize {
return _contentSize;
}

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [_layoutAttributes.allValues filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes * _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
if (CGRectIntersectsRect(rect, evaluatedObject.frame)) {
return YES;
}
return NO;
}]];
return attributes;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
return _layoutAttributes[indexPath];
}
@end

参考使用示例

1
2
3
4
5
6
7
8
  BICollectionViewFixedSizeLayout *_collectionViewFixedLayout = [[BICollectionViewFixedSizeLayout alloc] init];
_collectionViewFixedLayout.contentInsets = UIEdgeInsetsMake(10, 10, 10, 10);
_collectionViewFixedLayout.itemSize = CGSizeMake(36, 36);
_collectionViewFixedLayout.numberOfLinesPerPage = 3;
_collectionViewFixedLayout.minimalColumnSpacing = 10;
_collectionViewFixedLayout.anchorLastItemPerPage = YES;
[self.collectionView setCollectionViewLayout:_collectionViewFixedLayout animated:NO];

到此,layout的代码已经写完,需要注意的是在使用anchorLastItemPerPage 时 numberOfItemsInSection需要在数据源的count基础上增加page页数,在使用时也要注意获取到正确的数据index来使用。
简单的应用场景,整理记录一下,后续可能会进行优化😄。