ios 自动换行flowLayout
最近产品需要实现自动换行功能,在gitHub看了一下,虽然有不少,但都有那么一点不满足需求的,或者感觉用着不方便的。所以干脆自己写了一份,顺便在有时间时写了一份ios的版本,有兴趣想看android版的的可点击上面链接。
在这里先提供下载地址:https://github.com/lanqi-x/ios_FlowLayout
然后来个图先:
实现概要思路为:继承UIView,复写-(instancetype)initWithFrame:(CGRect)frame(代码创建)、-(instancetype)initWithCoder:(NSCoder *)aDecoder(xib或storyboard)、-(void)addSubview:(UIView *)view和- (void)layoutSubviews。
好,要开始吧啦吧啦了。ios版相对android版来说会比较简单,但相对的坑也比较多,一些get、set方法在此就不说了,代码注释还蛮多的,直接讲addView和layoutSubviews来个方法就好了。
1、首先addview
-(void)addSubview:(UIView *)view{
[self.subViewList addObject:view];
[super addSubview:view];
}
从代码就可以理解了,这个很简单,复写只为将view保存到我的数组之中,方便维护。
2、layoutSubviews
layoutSubviews比较复杂,首先oc也没有像java那样方便的内部类(普通类也可以,只是这里使用内部类在一些方法和属性上的调用会方便一些),所以我将每行的view和高度分别放在了两个数组之中。然后ios的View没有像android的view一样有测量方法,于是我把测量和子view的摆放全丢在layoutSubviews这个方法上了,所以我将分为计算和摆放两步来解释下我的实现思路。
(1)计算每行View的个数和每行的高度
这个也不难,就是对在addview存起来的view进行一次for循环,用sumWidth加入view以后所占的高度,即sumWidth+=view的宽度+view之间的间隔。情况一、sumWidth不大于flowLayout的宽,则将view添加到lineList中,比较该view的高度是否大于当前行高度height,如果大于则该行的高度等于view的高度。
情况二、sumWidth大于flowLayout的宽,那么将lineList加到rowList中记为一行,将height加到rowHeightList中记为该行的高度,height等于当前view的高度作为新一行的高度,重新new lineList,将view加到其中作为新的一行。
情况三、view为最后一个,那么不管够不够一行都记为一行,将lineList加到rowList中。
关键代码如下:
if (self.subViewList.count>0) {
//清空所有值,重新计算
[self.rowHightList removeAllObjects];
[self.rowList removeAllObjects];
//获取第一个view
NSMutableArray *lineList=[NSMutableArray array];
UIView* firstView=self.subViewList[0];
CGFloat sumWidth=self.horizontalSpace+firstView.frame.size.width;
CGFloat height=firstView.frame.size.height;
CGFloat flowWidth=self.frame.size.width;
[lineList addObject:firstView];
//计算每行放哪些View
for (int i=1; i<self.subViewList.count; i++) {
UIView* view=self.subViewList[i];
sumWidth=sumWidth+view.frame.size.width+self.horizontalSpace;
if (sumWidth >= flowWidth) {
//一行放不下了,记录为一行
[self saveLine:lineList lineHeight:height];
//重新创建一行
lineList=[NSMutableArray array];
[lineList addObject:view];
height=view.frame.size.height;
sumWidth=self.horizontalSpace+view.frame.size.width;
}else{
//一行还够放
[lineList addObject:view];
if (view.frame.size.height>height) {
height=view.frame.size.height;
}
}
//如果这是最后一个view了,不管够不够一行,都记为一行
if (i==self.subViewList.count-1) {
[self saveLine:lineList lineHeight:height];
}
}
ios的办法不需要像android那样调用view的layout方法,而是改变view的x,y值。这里使用rowList进行双重for循环,这里关键的有五点。
(1)每行的左上角坐标及起始y值,这里为行间距加上一行的高度(知道为什么上面计算那里要存每行的高度了吧)。
(2)每个view的x坐标,这里为view之间的间距加上上个view的宽度。
(3)view在该行居中显示,如果view的高度小于行的高度(这里再次用的行高度了),那么该view的y为y+=(lineHeight-view.width)/2(这里只是伪代码哈)
(4)判读是否超过FlowLayout的限定高度或最大行数,如果是,那么剩下的view都不显示(由于ios不像android,android有viewGroup,viewGroup不摆放子view时,子view是直接不显示的,而ios则用原始的frame值进行摆放,所以我这里为了不让他显示在界面上,直接将这些位置超过限制的子view进行了hidden)
(5)记录显示的最后一个view在subViewList的下标是多少。(用于当FlowLayout的高度设置为根据内容时重新计算FlowLayout的最终高度)
//摆放subView
CGFloat y=self.verticalSpace;
//一行一行取出
for (int i=0;i<self.rowList.count;i++) {
lineList=self.rowList[i];
CGFloat x=self.horizontalSpace;
//获取这一行的高度
CGFloat rowHeight=[self.rowHightList[i] floatValue];
if (i!=0) {
y=y+[self.rowHightList[i-1] floatValue]+self.verticalSpace;
}
if ((self.fastenHeight && y+[self.rowHightList[i] floatValue]>=self.frame.size.height)
|| i>=self.maxLine) {
[self dontShowLine:i];
break;
}
//摆放每一行的subView
for (UIView *item in lineList){
_lastShowIndex++;
[self setX:x changeView:item];
item.hidden=false;
if (item.frame.size.height<rowHeight) {
[self setY:y+((rowHeight-item.frame.size.height)/2.0) changeView:item];
}else{
[self setY:y changeView:item];
}
x=x+item.frame.size.width+self.horizontalSpace;
}
}
如果设置FlowLayout高度是根据内容决定的,那么重新调整一下FlowLayout的最终高度,首先获取显示的最后一个view的底部Y值(CGRectGetMaxY(lastItem.frame))加上行距作为FlowLayout最终的高度,这里做个两个处理一是将frame的height改为最终的高,二是如果FlowLayout存在NSLayoutAttributeHeight约束,那么就将其移除,添加新的NSLayoutAttributeHeight约束值为FlowLayout最终的高度。
if(!self.fastenHeight && self.lastShowIndex!=-1){
UIView *lastItem = self.subViewList[self.lastShowIndex];
CGRect rect=self.frame;
//计算flowLayout最终的高度
rect.size.height=CGRectGetMaxY(lastItem.frame) + self.verticalSpace;
self.frame = rect;
//// 修改约束,保证兄弟或父子控件的约束更新
NSArray* constrains = self.constraints;
for (NSLayoutConstraint* constraint in constrains) {
if (constraint.firstAttribute == NSLayoutAttributeHeight) {
[self removeConstraint:constraint];
[self addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeHeight multiplier:1.0 constant:rect.size.height]];
}
}
}
FlowLayout我的实现思路就是这样了,相比android版,这里还剩每行最后剩余空间的分配模式还没实现,以后有时间再补上。最后再提个小坑,就是最终修改FlowLayout的高度那里,如果你的高不是通过NSLayoutAttributeHeight确定,而是通过NSLayoutAttributeTop加NSLayoutAttributeBottom确定的,那么这里就不会更新了,那么如果FlowLayout下方有其他view,界面上就有可能会出现两个view覆盖的情况,因为个人觉得如果需求是想其高度根据内容决定的,那么一般应该是设置NSLayoutAttributeHeight约束的,所以这里不做处理。如果想处理,可以在FlowLayout中定义一个协议,在最后将计算得到的最终高度传给这个协议,提供给外部实时获取到FlowLayout的高度变化。
好,本文结束,如有说的不好的地方请多多包涵,也可在评论中指点一下。