iOS 自定义布局CollectionViewCell
前言
最近接了一个需求,就是在App中的搜索界面,用户可能会在搜索输入框输入或者很长或者很短的文字,然后开发者要对用户之前输入的内容要进行记录,就像淘宝以及京东等等App里面的搜索页面的效果一样,看图🔽
image.png看上面的搜索记录,有的是一行显示2个,有的是4个,或者只显示一个等等,情况都不一样,那么作为开发者的你,可能会有2个思路,一个是直接for循环来创建,计算内容宽度,如果大于屏幕宽度就换行呗,另一种是稍微高端一点的玩法就是使用UIcollectionView完全自定义实现Cell。可能路过的大佬也有其他更牛的想法,请大佬一定留言赐教,在此,先谢过各位大佬O(∩_∩)O哈哈~
先看下效果
image.png
实现方式一
-----来了老弟-----
对于第一种方式直接for循环的创建,我这就不多说了,思路很简单就是先计算出内容的宽度,然后与屏幕宽度做比较,在此我就直接show me code~~~
CGFloat markViewX = newCountWidth(45);
CGFloat btnMarkX = markViewX;
CGFloat markViewMaxW = SCREEN_WIDTH - markViewX * 2;
CGFloat btnMarkY = newCountWidth(98);
for (int x = 0; x < self.historyList.count; x++)
{
NSString *hotword = [self.historyList pf_objWithIndex:x];
NSString * btnText = [NSString stringWithFormat:@" %@ ",hotword];
//搜索内容按钮
UIButton * btnMark = [UIButton buttonWithType:UIButtonTypeCustom];
btnMark.layer.cornerRadius = newCountWidth(10);
btnMark.layer.masksToBounds = YES;
btnMark.backgroundColor = [UIColor colorFromHexString:@"#ecf0f6"];
btnMark.titleLabel.font = [UIFont systemFontOfSize:newCountWidth(39)];
[btnMark setTitleColor:[UIColor colorFromHexString:@"#333333"] forState: UIControlStateNormal];
[btnMark setTitle: btnText forState:UIControlStateNormal];
btnMark.tag = 2000 + x ;
[btnMark addTarget: self action:@selector(_clickBtnClick:) forControlEvents:UIControlEventTouchUpInside];
CGSize textSize = [btnText sizeWithfont:[UIFont systemFontOfSize:newCountWidth(39)] constrainedToSize: CGSizeMake(0, newCountWidth(96))];
if (btnMarkX + textSize.width >= markViewMaxW)
{
btnMarkX = markViewX;
btnMarkY += newCountWidth(96) + newCountWidth(27);
}
btnMark.frame = CGRectMake(btnMarkX, btnMarkY, textSize.width, newCountWidth(96));
[self addSubview:btnMark];
btnMarkX += textSize.width + newCountWidth(28);
}
其实代码也不多,仅供参考,精彩在后面~
实现方式二
使用UICollectionView,完全自定义cell的显示布局,对于collectionView的基本创建在此就不多说了,最关键的就是UICollectionViewFlowLayout,我们都知道要想自己去布局collectionViewCell,那就必须想法对UICollectionViewFlowLayout下手,在UICollectionViewFlowLayout中,我们可以使用下面的Api
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect; // return an array layout attributes instances for all the views in the given rect
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;
-
在这上面是有4个,在实现该需求中只使用前两个就可以了,看API的注释,就知道,是说,UICollectionView调用这四个方法来确定布局信息,通过实现-layoutAttributesForElementsInRect:返回辅助视图或装饰视图的布局属性,或以屏幕上需要的方式执行布局,且所有的item都可以通过indexPath进行布局属性实例,说白了,一句话就是通过上面的方法,你可以自己随心所欲的布局collecTionViewCell。
-
在layoutAttributesForElementsInRect:方法中,可以通过rect获取到你创建的所有colleViewCell中的attribute
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *array = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *itemArray = [NSMutableArray arrayWithCapacity:array.count];
for (UICollectionViewLayoutAttributes *attrs in array) {
UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:attrs.indexPath];
[itemArray addObject:attr];
}
return itemArray;
}
- 在layoutAttributesForItemAtIndexPath方法中,可以根据indexPath对单个的cell进行设置布局,就是修改cell的frame,而在该需求中是直接计算好cell的frame的值,然后直接重新创建,然后返回创建好的UICollectionViewLayoutAttributes。代码如下:
#pragma mark - 处理单个的Item的layoutAttributes
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
//collectionView 距离父视图左边的距离
CGFloat x = self.sectionInset.left;
//collectionView 距离父视图顶部的距离
CGFloat y = self.sectionInset.top;
//判断获得前一个cell的row
NSInteger preRow = indexPath.row - 1;
if(preRow >= 0)
{
if(self.yFrameArray.count > preRow)
{
x = [self.xFrameArray[preRow] floatValue];
y = [self.yFrameArray[preRow] floatValue];
}
NSIndexPath *preIndexPath = [NSIndexPath indexPathForItem:preRow inSection:indexPath.section];
CGFloat preWidth = [self.delegate obtainItemWidth:self widthAtIndexPath:preIndexPath];
x += preWidth + self.minimumInteritemSpacing;
}
//获取cell的宽度
CGFloat currentWidth = [self.delegate obtainItemWidth:self widthAtIndexPath:indexPath];;
//保证一个cell不超过最大宽度
currentWidth = MIN(currentWidth, self.collectionView.frame.size.width - self.sectionInset.left - self.sectionInset.right);
//根据cell的宽度+间距 计算cell的x和y坐标,如果大于一行则换行,否则不换行
if(x + currentWidth > self.collectionView.frame.size.width - self.sectionInset.right)
{
//超出范围,换行 计算x值, y值
x = self.sectionInset.left;
y += _rowHeight + self.minimumLineSpacing;
}
// 创建属性设置cell的frame
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attrs.frame = CGRectMake(x, y, currentWidth, _rowHeight);
/*
按照row将cell的frame的x和y插入进去
也可以使用insertObject:方法
*/
self.xFrameArray[indexPath.row] = @(x);
self.yFrameArray[indexPath.row] = @(y);
//NSLog(@"___xFrameArray___%@________yFrameArray___%@", self.xFrameArray, self.yFrameArray);
return attrs;
}
上面实现的主要思路就是,如果想要实现根据不同的内容宽度进行换行操作,其实就是计算好cell的frame的x值和y值,这里我们用两个数组来分别存储cell的frame的x和y,先获取到前一个cell的row,然后根据row拿到前一个cell的frame的x和y,然后用其x加上collectionView的间距minimumInteritemSpacing和内容宽度,然后和屏幕宽度比较一下,是不是要换行,如果大于屏幕宽度,则换行,x的值为sectionInset.left,就是collectionView距离父视图左边的距离,y值则是高度加上cell之间的行间距,计算好之后,将x,y,width,height,分别赋值给一个新创建的UICollectionViewLayoutAttributes,然后返回给layoutAttributesForItemAtIndexPath方法。
具体代码可参考,注释也很详细--------> 点我
swift 版本来实现该需求
使用swift来实现呢,其实思路是完全一样的,只是语法上是完全是不一样的,😝,毕竟是新进贵妃,最近又出来个SwiftUI,又是红了一把,堪比两宋离婚,李晨分手啊·有钱人的世界就是分分合合,我的世界只有“hello world”,说远了
核心代码放下面:
extension PFCollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
//var attributes : NSMutableArray = super.layoutAttributesForElements(in: rect) as! NSMutableArray
let array = super.layoutAttributesForElements(in: rect)
//var itemArray = Array(repeating: "", count: array!.count)
var itemArray = Array<Any>()
for attrs in array! {
let att : UICollectionViewLayoutAttributes = layoutAttributesForItem(at: attrs.indexPath)!
itemArray.append(att)
}
return array
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
//collectionView 距离父视图左边的距离
var x = self.sectionInset.left
//collectionView 距离父视图顶部的距离
var y = self.sectionInset.top
//判断获得前一个cell的row
let preRow = indexPath.row - 1
if preRow >= 0 {
if self.yFrameArray.count > preRow {
x = self.xFrameArray[preRow] as! CGFloat
y = self.yFrameArray[preRow] as! CGFloat
}
let preIndexPath = IndexPath.init()
if let prewidth = self.delegate?.obtainItemWidth(layout: self, atIndexPath: preIndexPath) {
x += prewidth + self.minimumLineSpacing
}
}
//获取cell的宽度
if let currentWidth = self.delegate?.obtainItemWidth(layout: self, atIndexPath: indexPath) {
//保证一个cell不超过最大宽度
let scrollViewFrame = self.collectionView?.frame.size.width
let currentItemWidth = min(currentWidth, scrollViewFrame! - self.sectionInset.left - self.sectionInset.right)
//根据cell的宽度+间距 计算cell的x和y坐标,如果大于一行则换行,否则不换行
if x + currentItemWidth > scrollViewFrame! - self.sectionInset.right {
//超出范围,换行 计算x值, y值
x = self.sectionInset.left
y += self.rowHeight
}
//创建属性设置cell的frame
let attrs : UICollectionViewLayoutAttributes = UICollectionViewLayoutAttributes.init(forCellWith: indexPath)
attrs.frame = CGRect(x: x, y: y, width: currentWidth, height: self.rowHeight)
/*
按照row将cell的frame的x和y插入进去
也可以使用insertObject:方法
*/
self.xFrameArray.insert(x, at: indexPath.row)
self.yFrameArray.insert(y, at: indexPath.row)
return attrs
}
return nil;
}
}
这里面实现思路也是一样的,也是通过下面这两个API
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect; // return an array layout attributes instances for all the views in the given rect
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
自己计算好cell的frame的x,y,width,height然后创建新的UICollectionViewLayoutAttributes,进行赋值,然后return,最新的Swift5 哦,在写swift时,有一个地方说下,就是根据文字内容计算文字宽度的时候,在网上查了下,swift5都报错了,后来改了下,下面放上代码:
let text : NSString = NSString(string: item)
let maxSize = CGSize(width: UIScreen.main.bounds.size.width - 2 * 30, height: CGFloat(MAXFLOAT))
let ww = NSString(string: text).boundingRect(with: maxSize, options: .usesFontLeading, attributes: [NSAttributedString.Key.font:UIFont.boldSystemFont(ofSize: 20)], context: nil).size.width
以上是全部内容,感谢各位大佬读到最后,然后感谢大佬们留个star,哈哈哈~~~