iOS不用AutoLayout也能实现自动布局的类(1)----MyLinearLayout
众所周知,对于IOS开发者来说,随着手机屏幕的尺寸在增多,不可避免的也需要考虑适配的问题了。这个问题在IOS6以前我们可以通过autoresizingMask和frame进行组合来解决视图伸缩、旋转的适配,但是这个方案不彻底还是需要编写很多的代码来完成;而在IOS6以后推出了AutoLayout的解决方案,这个方案的实现和可操作性太过于复杂和繁琐不仅编写的代码特别多,就是在XIB上进行布局约束也很麻烦和难以管理。于是乎有大牛就对AutoLayout进行了改造和精简推出了一些简化自动布局的框架比如:
Masonry库,CocoaUI库
我有幸的拜读和简单使用了一下这些库,发现确实有一些好处,可以简化视图布局的处理。但使用起来还是有一些麻烦,这些库都是对IOS的AutoLayout进行封装而已。因此本身如果不对自动布局不很了解的话也很容易绕道复杂的约束冲突中去;IOS8后因为有了4.7寸,5.5寸的屏幕后推出了sizeClass来进行布局.
那么有了这些后为什么我们还是不能满足??还要提出MyLinearLayout解决方案?
1.我们的应用还要支持到6.0以下。
2.我是一个老程序员不想学习新的布局语言。
3.AutoLayout,size Class语法过于晦涩而且不好控制,我的frame甚至无法使用了,我的动画特效也不好处理了。
4.MyLinearLayout的方法和使用逻辑简单,易用而且功能强大。
5.支持国产原创的库发展。
如果上述理由您没有一条同意的话那么可以关闭掉这篇文章了。
对于视图的布局来说,android系统的解决方案相对来说还是比较好的,对于android系统来说我们基本上不需要设置视图摆放的具体位置,因此一般也不需要设置视图的绝对位置值和绝对大小值,而是通过wrap_content,match_parent来指定视图本身的相对高度和宽度,同时通过LinearLayout, RelativeLayout,FrameLayout等布局来决定里面子视图的排列和摆放的位置,通过weight,padding,margin,gravity等属性来设置视图尺寸比例和排列对齐以及间距方面的东西, 而对于IOS来说我们以前编码时对于视图的布局总是通过setFrame来实现,用这种方法进行编程的最大的问题是我们必须在代码中写死很多位置的常量,而且还要自己进行计算高度和宽度以及边距等等,一般屏幕尺寸不同还需要对不同的尺寸进行处理,同样对于AutoLayout来说我们需要在代码里面编写大量的约束,造成的结果就是代码复杂和难以理解,对于维护来说更加是个灾难,而对于布局的微调更加是一个灾难。
基于上面的各种因素以及参考,MyLinearLayout横空出世了:https://github.com/youngsoft/MyLinearLayout
MyLinearLayout的实现充分参考了Android中的LinearLayout布局,但是却比LinearLayout更为强大,他几乎可以实现AutoLayout的所有功能甚至其不具备的功能。MyLinearLayout是一个基于流式布局的容器视图,我们只需要把子视图添加到MyLinearLayout中,并设置一些简单的约束参数那么就可以完成各种布局的要求了,而且后续中只要子视图的位置和大小进行变化都会触发容器视图里面的子视图进行重新布局。
我先简单的对MyLinearLayout里面的属性和函数进行介绍,然后我们再实现一些布局的场景的代码的实现。MyLinearLayout既可以用于编码实现有可以用在XIB中使用,但是在XIB中使用时请把AutoLayout的支持去掉,因为MyLinearLayout不是基于自动布局的。
在介绍MyLinearLayout之前,我们先要对视图扩展出一些属性,这些属性只用于MyLinearLayout中。我会在后面一一介绍这些属性以及用法。
@interface UIView(LayoutExt)
//下面4个属性用于指定视图跟他相关视图之间的间距,如果为0则是没有间距,如果>0 <1则是相对间距,是按父视图的比例算的,比如父视图是100,而左间距是0.1则值是10。如果大于等于1则是绝对间距
//一般当使用相对间距时主要用图是子视图的宽度和高度是固定的,只是边距随父视图的大小而调整。
@property(nonatomic,assign)CGFloat topMargin;
@property(nonatomic,assign)CGFloat leftMargin;
@property(nonatomic,assign)CGFloat bottomMargin;
@property(nonatomic,assign)CGFloat rightMargin;
@property(nonatomic,assign)CGFloat centerXOffset;
@property(nonatomic,assign)CGFloat centerYOffset;
@property(nonatomic,assign)CGPoint centerOffset;
//设定视图的高度在宽度是固定的情况下根据内容的大小而浮动,如果内容无法容纳的话则自动拉升视图的高度,如果原始高度高于内容则会缩小视图的高度。默认为NO,这个属性主要用UILabel,UITextView的多行的情况。
@property(nonatomic,assign,getter=isFlexedHeight)BOOL flexedHeight;
@end
@interface UIView(LinearLayoutExtra)
//比重,指定自定的高度或者宽度在父视图的比重。取值为>=0 <=1,这个特性用于平均分配高宽度或者按比例分配高宽度
@property(nonatomic,assign)CGFloat weight;
@end
这些属性只用在MyLinearLayout中和MyFrameLayout(框架布局,请浏览我的另外一篇文章)中才有意义。如果我们在XIB中进行布局的话我们可以在自定义数据设置界面指定这些属性。
接下来我们在介绍MyLinearLayout的定义,对于流式布局来说简单点就是从上到下或者从左到右,因此我们定义了垂直布局和水平布局两种样式。
//布局排列的方向
typedefenum : NSUInteger {
LVORIENTATION_VERT,
LVORIENTATION_HORZ,
} LineViewOrientation;
以及MyLinearLayout的属性:
@property(nonatomic,assign)LineViewOrientation orientation;
orientation = LVORIENTATION_VERT orientation = LVORIENTATION_HORZ
因为垂直布局和水平布局的实现都是一样的,下面的例子我都将以垂直布局进行举例,同时在没有特殊说明的情况下我会把MyLinearLayout的背景设置为灰色。
一、子视图间距设置以及自动调整大小的属性
要实现上面的布局需要键入下面的代码:
- MyLinearLayout *ll = [[MyLinearLayout alloc] initWithFrame:CGRectMake(0, 0, 100,200)];
- ll.backgroundColor = [UIColor grayColor];
- ll.orientation = LVORIENTATION_VERT;
- ll.leftMargin = 10;
- ll.wrapContentHeight = wrapContentHeight;
- UIView *v1 = [UIView new];
- v1.backgroundColor = [UIColor redColor];
- [ll addSubview:v1];
- v1.topMargin = 4;
- v1.leftMargin = 10;
- v1.width = 60;
- v1.height = 40;
- /* [v1 makeLayout:^(MyMaker *make) {
- make.top.equalTo(@4);
- make.left.equalTo(@10);
- make.width.equalTo(@60);
- make.height.equalTo(@40);
- }];*/
- UIView *v2 = [UIView new];
- v2.backgroundColor = [UIColor greenColor];
- [ll addSubview:v2];
- v2.topMargin = 6;
- v2.leftMargin = 20;
- v2.width = 40;
- v2.height = 60;
- //您也可以不设置width,height而是直接设置frame的宽度和高度
- UIView *v3 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 75, 30)];
- v3.backgroundColor = [UIColor blueColor];
- v3.topMargin = 3;
- v3.leftMargin = 15;
- v3.bottomMargin = 4;
- [ll addSubview:v3];
上面的代码中实现了垂直布局的代码,在这段代码中我们发现v1,v2,v3的frame中x,y值部分不需要指定和计算了,都默认设置为0,而改用topMargin和bottomMargin,leftMargin和rightMargin来指定视图之间的间距,这样一个好处是当某个子视图的高度变化时布局会自动重新进行子视图位置的排列,而不要手动进行调整。 同时可以发现虽然MyLinearLayout的高度设置为200,但实际高度确是147,这是怎么回事呢? 这是因为MyLinearLayout中有两个属性:
//高宽是否由子视图决定,这两个属性只对线性和相对布局有效,框架布局无效
@property(nonatomic,assign)BOOL wrapContentWidth;
@property(nonatomic,assign)BOOL wrapContentHeight;
这两个属性表示布局视图的宽度和高度是由子视图决定的,这个就像android的wrapContent一样,对于垂直布局来说默认wrapContentHeight设置为YES,而对于水平布局来说默认wrapContentWidth设置为YES。当然你也可以在垂直布局中将wrapContentHeight设置为NO。下面的例子就是:
- -(void)loadView
- {
- MyLinearLayout *test1ll = [MyLinearLayout new];
- test1ll.orientation = LVORIENTATION_HORZ; //水平布局
- test1ll.gravity = MGRAVITY_HORZ_CENTER; //本视图里面的所有子视图整体水平和垂直居中
- self.view = test1ll;
- //标尺视图
- UIView *v = [UIView new];
- v.backgroundColor = [UIColor blackColor];
- v.width = 10;
- v.height = 200;
- [test1ll addSubview:v];
- [test1ll addSubview:[self createView:NO]];
- [test1ll addSubview:[self createView:YES]];
- }
- -(UIView*)createView:(BOOL)wrapContentHeight
- {
- MyLinearLayout *ll = [[MyLinearLayout alloc] initWithFrame:CGRectMake(0, 0, 100,200)];
- ll.backgroundColor = [UIColor grayColor];
- ll.orientation = LVORIENTATION_VERT;
- ll.leftMargin = 10;
- ll.wrapContentHeight = wrapContentHeight;
- UIView *v1 = [UIView new];
- v1.backgroundColor = [UIColor redColor];
- [ll addSubview:v1];
- v1.topMargin = 4;
- v1.leftMargin = 10;
- v1.width = 60;
- v1.height = 40;
- /* [v1 makeLayout:^(MyMaker *make) {
- make.top.equalTo(@4);
- make.left.equalTo(@10);
- make.width.equalTo(@60);
- make.height.equalTo(@40);
- }];*/
- UIView *v2 = [UIView new];
- v2.backgroundColor = [UIColor greenColor];
- [ll addSubview:v2];
- v2.topMargin = 6;
- v2.leftMargin = 20;
- v2.width = 40;
- v2.height = 60;
- //您也可以不设置width,height而是直接设置frame的宽度和高度
- UIView *v3 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 75, 30)];
- v3.backgroundColor = [UIColor blueColor];
- v3.topMargin = 3;
- v3.leftMargin = 15;
- v3.bottomMargin = 4;
- [ll addSubview:v3];
- return ll;
- }
二、布局里面子视图的隐藏显示以及对UIScrollView的支持。
有时候有一些场景中,当某个或者某几个视图隐藏时,我们希望下面的视图能够自动往上移动以便填补空白,而当某个视图再次显示时下面的视图又再次往下移动,很幸运! MyLinearLayout是支持这种情况的(视图必须是MyLinearLayout的直接子视图才可以)。
另外因为我们有wrapContentHeight属性,因此我们可以把线性布局放入到一个ScrollView中(这又有点像android中的scrollview的方式)。来实现ScrollView的contentSize值的自动高度的调整。
这段代码如下:
- -(void)loadView
- {
- UIScrollView *scrollView = [UIScrollView new];
- self.view = scrollView;
- MyLinearLayout *ll = [MyLinearLayout new];
- ll.backgroundColor = [UIColor grayColor];
- ll.padding = UIEdgeInsetsMake(10, 10, 10, 10);
- ll.leftMargin = 0;
- ll.rightMargin = 0; //这里设置线性布局的左右边距都是0表示线性布局的宽度和scrollView保持一致
- UILabel *label = [UILabel new];
- label.leftMargin = 0;
- label.rightMargin = 0;
- label.flexedHeight = YES; //这个属性会控制在固定宽度下自动调整视图的高度。
- label.numberOfLines = 0;
- label.backgroundColor = [UIColor blueColor];
- label.text = @"这是一段可以隐藏的字符串,点击下面的按钮就可以实现文本的显示和隐藏,同时可以支持根据文字内容动态调整高度,这需要把flexedHeight设置为YES";
- [ll addSubview:label];
- UIButton *btn = [UIButton new];
- btn.leftMargin = 0;
- btn.rightMargin = 0;
- btn.height = 60;
- [btn setTitle:@"点击按钮显示隐藏文本" forState:UIControlStateNormal];
- [btn addTarget:self action:@selector(handleLabelShow:) forControlEvents:UIControlEventTouchUpInside];
- [ll addSubview:btn];
- UIView *bottomView = [UIView new];
- bottomView.backgroundColor = [UIColor greenColor];
- bottomView.leftMargin = 0;
- bottomView.rightMargin = 0;
- bottomView.height = 800;
- [ll addSubview:bottomView];
- [self.view addSubview:ll];
- }
- -(void)handleLabelShow:(UIButton*)sender
- {
- UIView *supv = sender.superview;
- NSArray *arr = supv.subviews;
- UILabel *lab = [arr objectAtIndex:0];
- if (lab.isHidden)
- lab.hidden = NO;
- else
- lab.hidden = YES;
- }
从代码中我们可以看出当我们要隐藏和显示某个子视图时直接设置子视图隐藏和取消隐藏而不需要再编码来调整整个视图的高度,不需要编码来移动下面兄弟视图的位置,不需要编码来调整父UIScrollView的contentSize来调整高度,我们还可以看到UIScrollView下只需要添加一个布局视图,同时我们还指定了布局视图的宽度和UIScrollView是保持一致的。同时我们还看到了UILabel中使用了扩展属性flexedHeight,这个属性设置为YES时,系统会在布局时自动根据指定的宽度来调整自己的高度,从而多行显示完所有的内容,这个属性非常的强大。
三、布局内视图位置的停靠以及布局的内部边距设定以及子视图大小的指定
有时候我们希望布局里面的所有子视图的位置都是固定的,比如所有子视图左对齐,或者居中对齐,或者居右对齐。这时候我们就需要用到布局中的如下属性了:
@property(nonatomic,assign)MarignGravity gravity;
这个属性用于指定布局中的所有子视图的停靠位置,一共有如下的定义:
MGRAVITY_NONE //不强制所有子视图的停靠方案而是子视图自己决定自己的停靠位置
MGRAVITY_HORZ_LEFT //水平居左停靠
MGRAVITY_HORZ_CENTER // 水平居中停靠
MGRAVITY_HORZ_RIGHT //水平居右停靠
MGRAVITY_HORZ_FILL //水平填充
MGRAVITY_VERT_TOP //垂直居顶停靠
MGRAVITY_VERT_CENTER //垂直居中停靠
MGRAVITY_VERT_BOTTOM //垂直居低停靠
MGRAVITY_VERT_FILL //垂直填充
MGRAVITY_CENTER //居中
MGRAVITY_FILL //填充
这些定义中当gravity为MGRAVITY_NONE时则表明不使用停靠策略,也就是按子视图自身的停靠策略,比如有一些场景中我们希望所有的子视图都左边对齐,那么我们可以设置布局视图的gravity的值为:MGRAVITY_HORZ_LEFT。而有时候我们又希望布局中的部分子视图左边对齐,部分右边对齐则我们不需要设置布局视图的gravity值而是分别对每个子视图自行设定。
有时候我们希望布局里面的所有子视图都跟布局保持一定的间距,这时候我们就可以用如下的属性:
@property(nonatomic,assign)UIEdgeInsets padding; //用来描述里面的子视图的离自己的边距,默认上下左右都是0
//这个是上面属性的简化设置版本。
@property(nonatomic,assign)CGFloat topPadding;
@property(nonatomic,assign)CGFloat leftPadding;
@property(nonatomic,assign)CGFloat bottomPadding;
@property(nonatomic,assign)CGFloat rightPadding;
padding属性用来描述里面的所有子视图跟自己保持的边界距离。
下面代码显示了垂直布局中的左中右三种停靠方式,以及四周的边距都设置为5,这样是不是进一步的简化了编码?
- -(UIView*)createView:(MarignGravity)gravity padding:(UIEdgeInsets)padding
- {
- //我们可以设置widthDime也可以设置
- MyLinearLayout *ll = [[MyLinearLayout alloc] initWithFrame:CGRectMake(0, 0, 100,200)];
- ll.orientation = LVORIENTATION_VERT;
- ll.leftMargin = 10;
- ll.gravity = gravity;
- ll.padding = padding;
- ll.backgroundColor = [UIColor grayColor];
- UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 60, 40)];
- v1.backgroundColor = [UIColor redColor];
- v1.topMargin = 4;
- [ll addSubview:v1];
- UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 40, 60)];
- v2.backgroundColor = [UIColor greenColor];
- v2.topMargin = 6;
- [ll addSubview:v2];
- UIView *v3 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 75, 30)];
- v3.backgroundColor = [UIColor blueColor];
- v3.topMargin = 3;
- v3.bottomMargin = 4;
- [ll addSubview:v3];
- return ll;
- }
- -(void)loadView
- {
- MyLinearLayout *test1ll = [MyLinearLayout new];
- test1ll.orientation = LVORIENTATION_HORZ; //水平布局
- test1ll.gravity = MGRAVITY_CENTER; //本视图里面的所有子视图整体水平居中停靠
- self.view = test1ll;
- [test1ll addSubview:[self createView:MGRAVITY_HORZ_LEFT padding:UIEdgeInsetsMake(5, 5, 5, 5)]];
- [test1ll addSubview:[self createView:MGRAVITY_HORZ_CENTER padding:UIEdgeInsetsMake(5, 5, 5, 5)]];
- [test1ll addSubview:[self createView:MGRAVITY_HORZ_RIGHT padding:UIEdgeInsetsMake(5, 5, 5, 5)]];
- }
上面的视图中我们可以通过为布局视图设置gravity的值来调整里面所有子视图的左右方向的停靠位置,同样我们也可以通过设置布局视图的gravity的值来调整所有子视图上下方向的停靠位置,见下面的图片:
代码如下:
- -(UIView*)createView:(MarignGravity)gravity
- {
- MyLinearLayout *ll = [[MyLinearLayout alloc] initWithFrame:CGRectMake(0, 0, 100,200)];
- ll.leftMargin = 10;
- ll.orientation = LVORIENTATION_VERT;
- ll.gravity = gravity;
- ll.backgroundColor = [UIColor grayColor];
- UIView *v1 = [UIView new];
- v1.backgroundColor = [UIColor redColor];
- v1.topMargin = 4;
- v1.leftMargin = v1.rightMargin = 0;
- v1.height = 40;
- [ll addSubview:v1];
- UIView *v2 = [UIView new];
- v2.backgroundColor = [UIColor greenColor];
- v2.topMargin = 6;
- v2.width = 40;
- v2.height = 60;
- [ll addSubview:v2];
- UIView *v3 = [UIView new];
- v3.backgroundColor = [UIColor blueColor];
- v3.topMargin = 3;
- v3.bottomMargin = 4;
- v3.width = 75;
- v3.height = 30;
- [ll addSubview:v3];
- return ll;
- }
- -(void)loadView
- {
- MyLinearLayout *test1ll = [MyLinearLayout new];
- test1ll.orientation = LVORIENTATION_HORZ; //水平布局
- test1ll.gravity = MGRAVITY_CENTER; //本视图里面的所有子视图整体水平居中停靠
- self.view = test1ll;
- [test1ll addSubview:[self createView:MGRAVITY_VERT_TOP]];
- [test1ll addSubview:[self createView:MGRAVITY_VERT_CENTER]];
- [test1ll addSubview:[self createView:MGRAVITY_VERT_BOTTOM]];
- }
从上面两段代码中我们可以通过gravity的值来设置布局视图内的所有视图的停靠方式,但是有的时候我们不希望由布局视图来统一控制停靠的方向(对于垂直布局的话是指左右方向,对于水平布局的话是指上下方向),而是希望由子视图自生来控制停靠的位置。这时候必须要将线性布局的gravity设置为MGRAVITY_NONE.
代码如下:
- -(void)loadView
- {
- MyLinearLayout *ll = [MyLinearLayout new];
- ll.orientation = LVORIENTATION_VERT;
- ll.gravity = MGRAVITY_VERT_CENTER;
- ll.backgroundColor = [UIColor grayColor];
- self.view = ll;
- //不再需要指定y的偏移值了。
- UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 50)];
- v1.backgroundColor = [UIColor redColor];
- v1.topMargin = 10;
- v1.leftMargin = 10;
- [ll addSubview:v1];
- UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
- v2.backgroundColor = [UIColor greenColor];
- v2.topMargin = 10;
- v2.centerXOffset = 0; //这里是居中
- [ll addSubview:v2];
- UIView *v3 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 50)];
- v3.backgroundColor = [UIColor blueColor];
- v3.topMargin = 10;
- v3.rightMargin = 10;
- [ll addSubview:v3];
- UIView *v4 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 50)];
- v4.backgroundColor = [UIColor yellowColor];
- v4.topMargin = 10;
- v4.bottomMargin = 10;
- v4.leftMargin = 5;
- v4.rightMargin = 5;
- [ll addSubview:v4];
- }
上面的四个子视图中v1设置了leftMargin=10表示左边边距为10, v2设置了centerXOffset = 0 表示水平居中, v3设置了rightMargin=10表示右边边距为10, 而v4设置了leftMargin=5,rightMargin = 5则表示两边的边距都是5,那么v4的宽度就会动态的调整。因此有时候我们想让子视图的宽度等于父视图时则可以设置如下:
v.leftMargin = 0;
v.rightMargin = 0;
在布局时,我们可以通过frame来设置子视图的高度和宽度的绝对值,我们可以通过扩展属性:
/*
下面两个属性是上面widthDime,heightDime的equalTo设置NSNumber类型值的简化版本,主要用在线性布局和框架布局里面
表示设置视图的宽度和高度,需要注意的是设置这两个值并不是直接设置frame里面的size的宽度和高度,而是设置的布局的高度和宽度值。
也就是说设置和获取值并不一定是最终视图在布局时的真实高度和宽度
v.width = 10 <=> v.widthDime.equalTo(@10)
*/
@property(nonatomic,assign)CGFloat width;
@property(nonatomic,assign)CGFloat height;
来设置视图的高度和宽度值,需要注意的是通过width,height设置的值并不是frame设置的高度和宽度值,而是在布局时才会生效的高度值和宽度。
我们也可以通过同时设置leftMargin 和rightMargin的值来指定视图的宽度值。但有时候我们希望能有更加强大的功能,比如希望某个子视图是布局视图宽度的50%或者80%或者希望比布局视图的宽度减少50等等。
这时候我们通过frame或者通过设置width就不能实现了,但是我们可以使用高级的子视图扩展属性。
//尺寸,如果设置了尺寸则以设置尺寸优先,否则以视图自身的frame的高宽为基准
@property(nonatomic,readonly) MyLayoutDime *widthDime;
@property(nonatomic,readonly) MyLayoutDime *heightDime;
上面的width和height设置是这两个属性设置的简化版本,我们可以看看MyLayoutDime的定义:
@interface MyLayoutDime :NSObject
//乘
-(MyLayoutDime* (^)(CGFloat val))multiply;
//加,用这个和equalTo的数组功能可以实现均分子视图宽度以及间隔的设定。
-(MyLayoutDime* (^)(CGFloat val))add;
//NSNumber, MyLayoutDime以及MyLayoutDime数组,数组的概念就是所有数组里面的子视图的尺寸平分父视图的尺寸。
-(MyLayoutDime* (^)(id val))equalTo;
@end
高级的widthDime和heightDime通常用在相对布局里面,但是现行布局也是支持。比如我们希望子视图的宽度和父视图相等则:
v.widthDime.equalTo(v.superView.widthDime)
如果我们希望子视图的宽度是父视图的50%则
v.widthDime.equalTo(v.superView.widthDime).multiply(0.5)
而我们希望子视图的宽度比父视图减少100则
v.widthDime.equalTo(v.superView.widthDime).add(-100)
我们希望子视图的宽度就是100则
v.widthDime.equalTo(@100) <==> v.width = 100
看如下布局:
代码如下:
- -(void)loadView
- {
- MyLinearLayout *ll = [MyLinearLayout new];
- ll.orientation = LVORIENTATION_VERT;
- ll.gravity = MGRAVITY_VERT_CENTER;
- ll.backgroundColor = [UIColor grayColor];
- self.view = ll;
- UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 50)];
- v1.backgroundColor = [UIColor redColor];
- v1.topMargin = 10;
- v1.leftMargin = v1.rightMargin = 0;
- [ll addSubview:v1];
- UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 50)];
- v2.backgroundColor = [UIColor greenColor];
- v2.topMargin = 10;
- v2.widthDime.equalTo(ll.widthDime).multiply(0.8); //父视图的宽度的0.8
- [ll addSubview:v2];
- UIView *v3 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 50)];
- v3.backgroundColor = [UIColor blueColor];
- v3.topMargin = 10;
- v3.widthDime.equalTo(ll.widthDime).add(-20); //父视图的宽度-20
- [ll addSubview:v3];
- UIView *v4 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 50)];
- v4.backgroundColor = [UIColor yellowColor];
- v4.topMargin = 10;
- v4.bottomMargin = 10;
- v4.leftMargin = 10;
- v4.rightMargin = 10;
- [ll addSubview:v4];
- }
上面的代码中我们使用了视图的高级扩展属性widthDime, heightDime,这两个通常用在相对布局MyRelativeLayout的处理中。具体的介绍可以看我的关于相对布局的处理的文章。
四、终极武器1:子视图的相对尺寸,以及布局视图的尺寸,复杂布局的处理。
在某些时候,我们知道了布局视图的高度的情况下,想平分里面所有子视图的高度,或者里面的子视图的高度我们只需要指定相对值而不需要指定绝对值,如果我们想在布局视图里面增加2个视图,其中一个视图占用布局视图的40%,而另外一个视图占用60%。因为如果支持子视图的相对高度的话,那么当布局视图进行缩放或者进行旋转时里面的子视图都会按照指定比例进行缩放,这时候我们需要用到上面视图的一个强大的扩展属性:weight了:
@property(nonatomic,assign)CGFloat weight;
这个视图的扩展属性的设置的范围是0到1表示子视图本身在布局视图中所占用的高度或者宽度的比例,下面我们来实现一个自动布局里面一个最经典的需求:
每个视图的间距都为20,分为上下2个部分,各占用50%,上面的2个视图各占用50%。
- -(void)loadView
- {
- //线性布局实现相对子视图
- MyLinearLayout *ll = [MyLinearLayout new];
- //保证容器和视图控制的视图的大小进行伸缩调整。
- ll.orientation = LVORIENTATION_VERT;
- ll.padding = UIEdgeInsetsMake(20, 20, 20, 20);
- ll.backgroundColor = [UIColor grayColor];
- MyLinearLayout *topll = MyLinearLayout.new;
- topll.orientation = LVORIENTATION_HORZ;
- topll.weight = 0.5;
- topll.leftMargin = topll.rightMargin = 0;
- UIView *topLeft = UIView.new;
- topLeft.backgroundColor = [UIColor redColor];
- topLeft.weight = 0.5;
- topLeft.topMargin = topLeft.bottomMargin = 0;
- [topll addSubview:topLeft];
- UIView *topRight = UIView.new;
- topRight.backgroundColor = [UIColor greenColor];
- topRight.weight = 0.5;
- topRight.topMargin = topRight.bottomMargin = 0;
- topRight.leftMargin = 20;
- [topll addSubview:topRight];
- [ll addSubview:topll];
- UIView *bottom = UIView.new;
- bottom.backgroundColor = [UIColor blueColor];
- bottom.weight = 0.5;
- bottom.widthDime.equalTo(ll.widthDime);
- bottom.leftMargin = bottom.rightMargin = 0;
- bottom.topMargin = 20;
- [ll addSubview:bottom];
- self.view = ll;
- }
从上面的代码可以看出,其中没有使用到任何绝对的位置和大小的数字,都是相对值,为了支持复杂的布局我们使用了MyLinearLayout的嵌套的方式来解决问题。
通过为子视图的weight的指定我们可以很灵活的对布局里面的子视图的高度进行设置,一个布局中我们可以设置某些子视图的绝对高度,也可以设置另外一些子视图的weight。比如:
1.某个线性布局有3个子视图,并且顶部和底部的视图的高度都是固定的,而中间的视图则占用布局的剩余高度则可以如下设置:
v1.frame = CGRectMake(0,0,x, 30)
v2.frame = CGRectZero
v2.weight = 1.0
v3.frame = CGRectMake(0,0,x,50)
2.某个线性布局有3个子视图,顶部的视图高度固定的,而底部两个视图则按剩下的高度的4:6来进行分配则可以设置如下:
v1.frame = CGRectMake(0,0,x, 30)
v2.frame = CGRectZero
v2.weight = 0.4
v3.frame = CGRectZero
v3.weight = 0.6
五、终极武器2:布局视图的高度和宽度完全由子视图来控制。
在垂直布局中,我们知道布局的高度可以由所有子视图动态调整,那么宽度是否也可以由子视图来决定呢?这是可以了!!当布局中某个子视图的宽度是确定的,我么可以选择由子视图里面最宽的那个视图来决定布局视图的宽度。而布局中的属性wrapContentXXX则类似于android的wrap_content的值:
//高宽是否由子视图决定,这两个属性只对线性和相对布局有效,框架布局无效
@property(nonatomic,assign)BOOL wrapContentWidth;
@property(nonatomic,assign)BOOL wrapContentHeight;
- -(void)loadView
- {
- [super loadView];
- //布局视图中不需要指定宽度,而是由最大子视图决定宽度
- MyLinearLayout *ll = [[MyLinearLayout alloc] initWithFrame:CGRectMake(100, 100, 0,0)];
- ll.orientation = LVORIENTATION_VERT;
- ll.wrapContentWidth = YES; //这句话表示宽度的尺寸由最大的子视图的宽度决定。
- ll.backgroundColor = [UIColor grayColor];
- UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 90, 40)]; //这个子视图最宽 10+90+20 父视图的宽度为110
- v1.backgroundColor = [UIColor redColor];
- v1.leftMargin = 10;
- v1.rightMargin = 20;
- v1.topMargin = 4;
- [ll addSubview:v1];
- UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 40, 60)];
- v2.backgroundColor = [UIColor greenColor];
- v2.topMargin = 6;
- [ll addSubview:v2];
- UIView *v3 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 75, 30)];
- v3.backgroundColor = [UIColor blueColor];
- v3.topMargin = 3;
- v3.bottomMargin = 4;
- [ll addSubview:v3];
- [self.view addSubview:ll];
- }
上面的视图中可以看到布局视图的宽度是第一个视图的宽度外加上leftMargin,rightMargin的总值,因为垂直布局中的wrapContentHeight默认是设置为YES的。
有的时候我们希望让某个布局视图设置为某个非布局视图的子视图,比如我们希望建立一个布局视图全部覆盖到某个view下,这时候我们一样的可以在将布局视图添加到非布局视图下时通过指定自己的leftMargin,rightMargin, topMargin,bottomMargin来设置布局视图距离父视图的边距值,一旦将某个布局视图放入到非布局视图时并且又希望这个布局视图随非布局视图的大小而调整时,需要注意的是要将wrapContentHeight,wrapContentWidth 设置为NO,因为如果设置为YES的话就会产生布局的冲突。
六、终极武器3:视图之间的间距也可以是相对值。
上面的所有布局中,我们可以让布局视图随着子视图的尺寸进行大小的调整,也可以让子视图随着布局视图的尺寸进行大小的调整,也可以使用weight进行子视图的按比例尺寸分配,也可以使用gravity进行子视图的位置和尺寸的控制。但是前面的代码中所有的间距指定的都是固定值,正是因为间距部分是固定值,因此我们还是无法好好的适配不同尺寸的屏幕,比如有时候我们希望子视图的尺寸是固定的,但是视图之间的间距是随着屏幕尺寸的大小而调整。在比如下面的登录对话框
我们要求底部版权部分固定在底部,并且有固定的底部边距,而中间的图标和账号输入框的间距之间则需要根据布局的大小的调整而进行缩放。这时候因为子视图的高度是固定的,而间距是浮动的,因此解决的方法就是我们不设置固定的间距,而是设置浮动的间距,将间距按一定比例进行指定。通过采用间距使用比例的方法我们可以很容易的实现则不同屏幕尺寸上以及横向和纵向屏幕上进行完美的适配,这样的话我们是不是不再需要size class了。 上面的子视图扩展属性中我们已经看到了四个边距值是可以设置相对边距的,当我们把边距设置为>0而小于1的话,则表明是按比例来设置间距。我们可以看看如下代码是如何实现上述功能的:
- -(void)loadView
- {
- MyLinearLayout *ll = [MyLinearLayout new];
- ll.backgroundColor = [UIColor grayColor];
- ll.autoAdjustSize = NO;
- ll.gravity = MGRAVITY_HORZ_CENTER;
- //头像
- UIImageView *imgView = [UIImageView new];
- imgView.image = [UIImage imageNamed:@"user"];
- imgView.backgroundColor = [UIColor whiteColor];
- [imgView sizeToFit];
- imgView.topMargin = 0.45;
- [ll addSubview:imgView];
- //输入框
- UITextField *txtField = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 200, 40)];
- txtField.borderStyle = UITextBorderStyleLine;
- txtField.placeholder = @"请输入用户名称";
- txtField.backgroundColor = [UIColor whiteColor];
- txtField.topMargin = 0.1;
- txtField.bottomMargin = 0.45;
- [ll addSubview:txtField];
- UILabel *lab = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
- lab.bottomMargin = 20;
- lab.text = @"版权所有 XXX 公司";
- [lab sizeToFit];
- [ll addSubview:lab];
- self.view = ll;
- }
上面的代码中我们可以看到底部子视图的bottomMargin使用的是固定的间距也就是保证在底部,而头像和布局,头像和账号,以及账号和底部则采用的是相对的margin值,这样就是实现了上述的功能了,这样是不是很简单。
通过相对间距和子视图的weight属性,我们还能实现很多强大的功能,比如:
1.我们想让某个子视图跟父视图的边距始终保持在整体宽度的20%左右,那么我们只需要为子视图的leftMargin = 0.2就可以了。
2.我们在一个垂直布局中有3个子视图,并且要求这三个子视图的高度保持一致,同时间距也和高度保持一致,也就是平均分布三个子视图。
代码如下:
- -(void)loadView
- {
- MyLinearLayout *ll = [MyLinearLayout new];
- ll.backgroundColor = [UIColor grayColor];
- ll.wrapContentHeight = NO;
- ll.leftPadding = 10;
- ll.rightPadding = 10;
- UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 100)];
- v1.backgroundColor = [UIColor redColor];
- v1.leftMargin = v1.rightMargin = 0;
- [ll addSubview:v1];
- UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 50)];
- v2.backgroundColor = [UIColor greenColor];
- v2.leftMargin = v2.rightMargin = 0;
- [ll addSubview:v2];
- UIView *v3 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 70)];
- v3.backgroundColor = [UIColor blueColor];
- v3.leftMargin = v3.rightMargin = 0;
- [ll addSubview:v3];
- //每个视图的高度保持原始值,剩余的部分平分间距
- // [ll averageMargin:NO];
- //会把视图和间距都平分,即使设置了高度也无效。
- [ll averageSubviews:YES];
- self.view = ll;
- }
三个子视图和间距的高度都是平均的。看如下函数:
-(void)averageSubviews:(BOOL)centered;
这个函数用来指定将子视图和间距平均分配,centered表示是否整体居中,也就是是否保留顶部和底部的边距。
-(void)averageMargin:(BOOL)centered;
这个函数要求每个子视图都具有固定的高度或者宽度,而是把所有剩余的间距全部平分,同样centered也表示视图是否居中。
需要注意的是上面两个函数只对之前添加的视图有效,后续添加的视图是无效的。
七、终极武器4:UITableView的替代品。
实践中我们经常使用UITableView来布局一些静态的CELL,这种方式在某些场合确实很方面,既可以重用又可以很方便的使用滚动视图的功能,但是静态CELL一个最致命的问题是,有时候我们的某个CELL的高度是要求动态变化的,而且有时候我们的CELL里面有UILabel,里面的内容假如很长的话需要换行显示,从而调整CELL的高度,于是我们就只能在:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
动态计算CELL的高度,然后又在CELL中进行各种麻烦的布局,而且这个问题AutoLayout是无法解决的。我象形很多人都会在这个问题上写很多代码。还有一个场景是我们的CELL之间是可以设置分割线的,但是有时候有些需求是我们要求顶部没有线,底部没有线等等一些奇怪的问题,于是乎我们就需要在CELL中插入背景视图并且把CELL的分割线属性取消来解决这个问题,还有一个场景是因为CELL是可以复用的,一旦CELL不可见系统自动删除子视图的内容(注意这时候子视图上的一些状态是无法保留的),然后出现时你又要重新根据状态来设置CELL中的内容,这样你就需要单独的保存各种视图的状态数据。
其实对于一些静态CELL的情况下因为CELL的数量不多,我们完全不需要考虑内存以及复用的问题。我们只要在UIScrollView上直接线性布局就可以了。这时候我们就可以祭出MyLayout的两个大杀器了:
边界线绘制以及布局的触摸处理事件,
通过这两个属性就可以完全把一个MyLinearLayout当做一个CELL来使用了。
边界线绘制为我们实现四个边界的线条的绘制,他支持线条的颜色,粗细,缩进,立体,点线的绘制:
//指定四个边界线的绘制。
@property(nonatomic,strong)MyBorderLineDraw *leftBorderLine;
@property(nonatomic,strong)MyBorderLineDraw *rightBorderLine;
@property(nonatomic,strong)MyBorderLineDraw *topBorderLine;
@property(nonatomic,strong)MyBorderLineDraw *bottomBorderLine;
//同时设置4个边界线。
@property(nonatomic,strong)MyBorderLineDraw *boundBorderLine;
然后MyBorderLineDraw的定义如下:
@interface MyBorderLineDraw :NSObject
@property(nonatomic)UIColor *color; //颜色
@property(nonatomic)UIColor *insetColor; //嵌入颜色,用于实现立体效果
@property(nonatomic,assign)CGFloat thick; //厚度,默认为1
@property(nonatomic,assign)CGFloat headIndent; //头部缩进
@property(nonatomic,assign)CGFloat tailIndent; //尾部缩进
@property(nonatomic,assign)CGFloat dash; //虚线的点数如果为0则是实线。
-(id)initWithColor:(UIColor*)color;
@end
通过边界线我们可以实现布局视图四个方向的边界线的绘制。布局视图的另外两个属性如下:
//高亮的背景色,我们支持在布局中执行单击的事件,用户按下时背景会高亮.只有设置了事件才会高亮
@property(nonatomic,strong)UIColor *highlightedBackgroundColor;
//设置单击触摸的事件,如果target为nil则取消事件。
-(void)setTarget:(id)target action:(SEL)action;
没错!我们可以为布局视图设置单击处理事件!以及单击时的高亮显示颜色。这两个功能是代替
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
的强有力武器,只要我们把一个垂直MyLinearLayout作为UIScrollView的子视图,并且垂直的MyLinearLayout中加入多个水平的MyLinearLayout子视图(起到CELL的功能),然后为每个水平的子布局视图添加时间处理函数就好了。 废话这么多,上界面吧。代码因为比较多就不贴出来了,直接看DEMO中的就可以了。
八、总结
MyLinearLayout的功能基本就介绍完成了,最后需要总结的是: MyLinearLayout可以完全胜任屏幕的旋转,各种尺寸的完美适配。各种版本操作系统的完美适配,开发简单易用而且功能强大,而且他不是基于AutoLayout的也不是基于Size Class,没有版本限制,也不需要学习新的布局知识。
最后欢迎大家到我的github中去下载库和DEMO。