MyLayout和TangramKit是一套基于frame之上的UI界面布局库的OC版本和Swift版本。目前最新版本升级为MyLayout1.7.0和TangramKit1.4.0。
👉OC1.7.0: https://github.com/youngsoft/MyLinearLayout
👉Swift1.4.0: https://github.com/youngsoft/TangramKit
这次升级的主要目的是为了和AutoLayout结合的更加紧密。
这不是一篇推广文,而是介绍AutoLayout和MyLayout&TangramKit是如何实现视图尺寸自适应的以及二者是如何结合在一起的。所以希望您耐着性子继续往下看👇👇👇👇👇
AutoLayout的尺寸自适应
AutoLayout中有两种类型的尺寸自适应:一类是以UILabel和UITextView为代表视图的尺寸自适应,这类视图中的宽度和高度有时候需要根据自身内容来确定自己的宽度和高度。也就是说这类视图有自己的固有内容尺寸(intrinsicContentSize)。在UIView类中提供了一个可供重载的方法:
- (CGSize)intrinsicContentSize NS_AVAILABLE_IOS(6_0);
如果某类视图有自己的固有内容尺寸则需要重载这个方法的实现。这个方法返回根据自身内容而计算出来的固有内容尺寸的size,如果没有固有内容尺寸则方法返回一个特殊的默认值UIViewNoIntrinsicMetric(-1)。很明显UIView类的返回值是默认值,而UILabel和UITextView这些类则重载了这个方法并返回了根据自身内容计算出来的尺寸。
当一个视图有自己的固有内容尺寸时,就不需要再为视图设置宽度或者高度约束。这也就是为什么一般情况下不对UILabel视图设置宽度和高度约束时系统也能正常完成布局。系统内部的实现中如果布局引擎在布局时发现某个视图没有设置高度或者宽度约束那么就会去调用这个视图的intrinsicContentSize方法,如果这个方法返回了正常的尺寸则视图就按这个尺寸来进行渲染和展示,如果这个方法返回值的某个维度是UIViewNoIntrinsicMetric则表明某个维度也没有固有内容尺寸从而实现约束缺失的现象。
另外一类是一些容器视图的高度或者宽度希望根据其中的子视图来确定。比如一些界面中有父视图的尺寸由子视图的尺寸来确定的;还比如UIScrollView中为了能实现滚动需要根据添加到里面的子视图来调整contentSize的尺寸;又比如某些UITableViewCell中的高度是动态的,其高度尺寸是由里面的子视图来确定的。在这些类中并没有重载intrinsicContentSize的实现,所以需要提供一种新的设置方法来实现这种尺寸自适应的能力。
1. 容器视图实现尺寸自适应
对于一个容器父视图来说,当要实现父视图的尺寸依赖所有子视图的尺寸来实现自适应时,要设置的约束依赖不是通过尺寸约束来实现而是通过位置约束来实现。假设有如下的布局:
我们希望父容器视图S的尺寸是自适应的,那么就需要设置S视图的右边边界等于子视图B的右边边界,同时需要设置S视图的底部边界等于子视图C的底部边界。可以看出来要实现父容器视图S的尺寸自适应时不是通过设置宽度和高度的尺寸依赖来实现的而是通过设置让父视图的边界依赖于某个子视图的边界来实现的。具体代码展示如下:
//这里忽略了视图的创建代码。
//本文对AutoLayout进行约束设置都是用iOS9以后所提供的进行约束设置的简易方法。
[A.leftAnchor constraintEqualToAnchor:S.leftAnchor constant:10].active = YES;
[A.topAnchor constraintEqualToAnchor:S.topAnchor constant:10].active = YES;
[A.widthAnchor constraintEqualToConstant:100].active = YES;
[A.heightAnchor constraintEqualToConstant:30].active = YES;
[B.leftAnchor constraintEqualToAnchor:S.leftAnchor constant:80].active = YES;
[B.topAnchor constraintEqualToAnchor:A.bottomAnchor constant:20].active = YES;
[B.widthAnchor constraintEqualToConstant:200].active = YES;
[B.heightAnchor constraintEqualToConstant:30].active = YES;
[C.leftAnchor constraintEqualToAnchor:S.leftAnchor constant:30].active = YES;
[C.topAnchor constraintEqualToAnchor:B.bottomAnchor constant:20].active = YES;
[C.widthAnchor constraintEqualToConstant:50].active = YES;
[C.heightAnchor constraintEqualToConstant:40].active = YES;
//假设S是添加在某个视图控制器中。
[S.leftAnchor constraintEqualToAnchor:self.view.leftAnchor constant:20].active = YES;
[S.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:90].active = YES;
//右边边界依赖B子视图的右边,下边边界依赖C子视图的下边实现尺寸自适应
[S.rightAnchor constraintEqualToAnchor:B.rightAnchor constant:20].active = YES;
[S.bottomAnchor constraintEqualToAnchor:C.bottomAnchor constant:20].active = YES;
可以看出这种实现的机制有一定的局限性!那就是当添加或者删除子视图时以及调整了某个子视图的位置和尺寸时就需要重新调整父视图的自适应约束设置。
2.UIScrollView的滚动
对于UIScrollView来说需要设置contentSize来实现滚动的能力。但是基于约束设置的布局体系来说,因为很多约束都是通过依赖来实现的,因此要计算contentSize并不是那么的容易和简单。为此当UIScrollView要和AutoLayout进行结合使用并实现滚动能力的话就不能直接将所有子视图都添加到UIScrollView中去, 而是需要中间建立一个容器视图,首先将容器视图添加到UIScrollView中去,然后再将所有子视图添加到容器视图中去。在设置约束依赖时将容器视图的上下左右分别依赖UIScrollView视图的上下左右边界,如果需要上下滚动则将容器视图中的最底部子视图的底部边界依赖容器视图的底部边界。如果不需要上下滚动则改为将容器视图的高度等于UIScrollView视图高度即可。 如果需要左右滚动则将容器视图中的最右边子视图的右边边界依赖于容器视图的右边边界。如果不需要水平滚动则改为将容器视图的宽度等于UIScrollView视图的宽度。通过这样的设置后UIScrollView视图的contentSize将得到自动的计算。下面是具体的实例代码:
//1.创建一个滚动视图,并设置好约束,这个约束可以是AutoLayout也可以是frame的,这里为了简单就用frame。
UIScrollView *scrollView = [UIScrollView new];
[self.view addSubview:scrollView];
scrollView.frame = CGRectMake(100, 100, 100, 100);
//2.创建一个容器视图, 这个容器视图放入滚动视图中,保证滚动视图只有一个容器子视图。
UIView *containerView = [UIView new];
containerView.translatesAutoresizingMaskIntoConstraints = NO;
containerView.backgroundColor = [UIColor orangeColor];
[scrollView addSubview:containerView];
//3.将所有的子视图A,B,C都添加到容器视图中。
UILabel *A = [UILabel new];
A.text = @"A";
A.translatesAutoresizingMaskIntoConstraints = NO;
A.backgroundColor = [UIColor redColor];
[containerView addSubview:A];
UILabel *B = [UILabel new];
B.text = @"B";
B.translatesAutoresizingMaskIntoConstraints = NO;
B.backgroundColor = [UIColor greenColor];
[containerView addSubview:B];
UILabel *C = [UILabel new];
C.text = @"C";
C.translatesAutoresizingMaskIntoConstraints = NO;
C.backgroundColor = [UIColor blueColor];
[containerView addSubview:C];
//4.分别设置容器视图中子视图的约束依赖。
[A.leftAnchor constraintEqualToAnchor:containerView.leftAnchor constant:10].active = YES;
[A.topAnchor constraintEqualToAnchor:containerView.topAnchor constant:10].active = YES;
[A.widthAnchor constraintEqualToConstant:100].active = YES;
[A.heightAnchor constraintEqualToConstant:30].active = YES;
[B.leftAnchor constraintEqualToAnchor:containerView.leftAnchor constant:80].active = YES;
[B.topAnchor constraintEqualToAnchor:A.bottomAnchor constant:20].active = YES;
[B.widthAnchor constraintEqualToConstant:200].active = YES;
[B.heightAnchor constraintEqualToConstant:30].active = YES;
[C.leftAnchor constraintEqualToAnchor:containerView.leftAnchor constant:30].active = YES;
[C.topAnchor constraintEqualToAnchor:B.bottomAnchor constant:20].active = YES;
[C.widthAnchor constraintEqualToConstant:50].active = YES;
[C.heightAnchor constraintEqualToConstant:40].active = YES;
//5.设置容器视图的依赖约束,让容器视图的四个边界分别等于滚动视图的四个边界,这里必须要这样设置。
[containerView.leftAnchor constraintEqualToAnchor:scrollView.leftAnchor].active = YES;
[containerView.topAnchor constraintEqualToAnchor:scrollView.topAnchor].active = YES;
[containerView.rightAnchor constraintEqualToAnchor:scrollView.rightAnchor].active = YES;
[containerView.bottomAnchor constraintEqualToAnchor:scrollView.bottomAnchor].active = YES;
//6.关键的一步,如果需要上下滚动则将容器视图中的最底部子视图这里是C的底部边界依赖于容器视图的底部边界。如果不需要上下滚动则不要这样设置,而是改为将容器视图的高度等于滚动视图高度。
[C.bottomAnchor constraintEqualToAnchor:containerView.bottomAnchor].active = YES;
//[containerView.heightAnchor constraintEqualToAnchor:scrollView.heightAnchor].active = YES;
//7.关键的一步,如果需要左右滚动则将容器视图中的最右部子视图这里是B的右边边界依赖于容器视图的右边边界。如果不需要水平滚动则不要这样设置,而是改为将容器视图的宽度等于滚动视图的宽度
[B.rightAnchor constraintEqualToAnchor:containerView.rightAnchor].active = YES;
//[containerView.widthAnchor constraintEqualToAnchor:scrollView.widthAnchor].active = YES;
如果是使用Storyboard来设置约束依赖的步骤和流程也是一样的。上面的约束设置实现视图滚动的机制也有一定的局限性!那就是一旦在容器视图中添加子视图时就需要重新调整容器视图的右边界和下边界的约束依赖。这就需要将旧的边界约束依赖记住,并在设置新的边界依赖前删除旧的约束依赖。
3.UITableViewCell的高度自适应
UITableViewCell要实现高度自适应,需要在UITableViewDelegate中的方法:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewAutomaticDimension;
}
实现中针对某个cell返回一个特定高度值UITableViewAutomaticDimension
。然后在UITableViewCell的派生类的视图代码布局处或者在-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
方法中进行特殊约束设置即可。在上面的第1节中有介绍如何将一个容器视图的尺寸设置为自适应,而一般情况下在编写UITableViewCell的布局代码时,都将所有的子视图添加到contentView这个视图中,因此要实现UITableViewCell的高度自适应时,只需要将contentView当做是一个容器视图,然后按照第1节中介绍的布局约束设置方法就可以实现高度自适应了。
MyLayout&TangramKit的尺寸自适应
MyLayout&TangramKit中的一个重要的能力是支持布局视图尺寸自适应的自动计算,也就是说布局视图的宽度或者高度可以根据子视图的尺寸来自行确定,而不需要去明确的依赖某个子视图来实现这种能力。对于MyLayout来说可以设置布局视图的wrapContentHeight或者wrapContentWidth为YES来实现这种能力,而对于TangramKit来说可以设置布局视图的tg_width, tg_height的值为.wrap来实现这种能力。比如一个布局父视图S中有三个子视图A,B,C。要求S的高度和宽度根据三个子视图的高度和宽度自适应,那么只需要将布局视图S的约束设置为如下:
//OC版本
S.wrapContentSize = YES;
//Swift版本
S.tg_size(width:.wrap, height:.wrap)
1.容器视图实现尺寸自适应
在MyLayout&TangramKit中的定义出了特殊的布局视图这个概念。所有为子视图设置的约束都必须放入到一个布局视图中才有效。整个布局框架提供了多种布局视图,每种布局视图中的子视图都将按照特定的规则进行排列和布局。当布局视图这个容器视图要实现尺寸自适应时就非常简单,它不需要依赖任何对子视图的约束依赖,而只需要将布局视图的尺寸设置为wrap即可。就以上面的图片例子用MyLayout&TangramKit来实现来说,可以将S视图定义为一个垂直线性布局视图,而将A,B,C三个子视图添加到布局视图中即可。下面是具体实现的布局部分的代码:
------------------------------------------------
//OC版本,S是一个垂直线性布局
A.myLeft = 10;
A.myTop = 10;
A.mySize = CGSizeMake(100,30);
B.myLeft = 80;
B.myTop = 20;
B.mySize = CGSizeMake(200, 30);
C.myLeft = 30;
C.myTop = 20;
C.mySize = CGSizeMake(50, 40);
//只需要将布局视图的相应属性设置为YES即可,不需要依赖于特定子视图。
S.wrapContentSize = YES;
------------------------------------------------
//Swift版本,S是一个垂直线性布局
A.tg_origin(x:10,y:10).and().tg_size(width:100,height:30)
B.tg_origin(x:80,y:20).and().tg_size(width:200,height:30)
C.tg_origin(x:30,y:20).and().tg_size(width:50,height:40)
//布局视图的尺寸根据子视图自适应。
S.tg_size(width:.wrap, height:.wrap)
因为MyLayout&TangramKit中的尺寸自适应约束不需要明确依赖的某个子视图,因此当布局视图中的子视图有变化时系统会自动重新进行布局视图的尺寸计算,而不需要做任何调整,这是使用MyLayout&TangramKit的最大的一个优势!
2.UIScrollView的滚动
MyLayout&TangramKit对于处理和UIScrollView进行结合时进行特殊处理,当将一个布局视图添加到滚动视图时,布局系统内部会负责处理滚动视图的contentSize。要实现UIScrollView滚动时,只需要在一个滚动视图内添加一个布局视图,然后将所有其他子视图都添加到这个布局视图中去,这个和上面的AutoLayout的处理方式是一样的,最后将布局视图的尺寸自适应属性设置为YES就可以了。具体实现的OC代码如下:
//1.创建一个滚动视图,并设置好约束,这个约束可以是AutoLayout也可以是frame的,这里为了简单就用frame。
UIScrollView *scrollView = [UIScrollView new];
[self.view addSubview:scrollView];
scrollView.frame = CGRectMake(100, 100, 100, 100);
//2.创建一个容器视图, 这个容器视图放入滚动视图中,保证滚动视图只有一个容器子视图。
MyLinearLayout *containerView = [MyLinearLayout new];
containerView.backgroundColor = [UIColor orangeColor];
//设置容器布局视图的尺寸自适应属性为YES。
containerView.wrapContentSize = YES;
[scrollView addSubview:containerView];
//3.将所有的子视图A,B,C都添加到容器视图中。
UILabel *A = [UILabel new];
A.text = @"A";
A.backgroundColor = [UIColor redColor];
[containerView addSubview:A];
UILabel *B = [UILabel new];
B.text = @"B";
B.backgroundColor = [UIColor greenColor];
[containerView addSubview:B];
UILabel *C = [UILabel new];
C.text = @"C";
C.backgroundColor = [UIColor blueColor];
[containerView addSubview:C];
//4.分别设置容器视图中子视图的约束依赖。
A.myLeft = 10;
A.myTop = 10;
A.mySize = CGSizeMake(100,30);
B.myLeft = 80;
B.myTop = 20;
B.mySize = CGSizeMake(200, 30);
C.myLeft = 30;
C.myTop = 20;
C.mySize = CGSizeMake(50, 40);
//5.然后就没有然后了!用MyLayout不需要进行特殊的附加设置!!
因为MyLayout&TangramKit中的尺寸自适应约束不需要明确依赖某个子视图,因此当布局视图中的子视图有变化时系统会自动重新进行布局视图的尺寸计算,而当布局视图的尺寸变化时又会调整UIScrollView的contentSize。
3. UITableViewCell的高度自适应
UITableViewCell要实现高度自适应,需要在UITableViewDelegate中的方法:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewAutomaticDimension;
}
实现中针对某个cell返回一个特定高度值UITableViewAutomaticDimension。然后在UITableViewCell的派生类中建立一个根布局视图,这个根布局视图作为子视图添加到contentView中代码如下:
//假设根布局视图是一个垂直线性布局视图。
self.rootLayout= [MyLinearLayout linearLayoutWithOrientation:MyOrientation_Vert];
self.rootLayout.cacheEstimatedRect = YES; //提供缓存加速计算
self.rootLayout.myHorzMargin = 0; //布局宽度等于contentView的宽度
self.rootLayout.wrapContentHeight = YES; //布局视图高度自适应。
[self.contentView addSubview:self.rootLayout];
//这里将所有子视图都添加到rootLayout中,并设置约束。
然后在UITableViewCell的派生类中重载视图的方法:
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority
{
//系统在对UITableVeiwCell进行布局时会调用systemLayoutSizeFittingSize方法来得到视图的尺寸,我们把计算cell尺寸的任务交由布局视图的sizeThatFits方法来完成。
return [self.rootLayout sizeThatFits:targetSize];
}
MyLayout&TangramKit和AutoLayout的结合
MyLayout&TangramKit的布局体系是基于原生的frame的计算来实现布局,而AutoLayout则不再依赖frame而是依赖视图之间的约束来是实现布局。这里只介绍将MyLayout&TangramKit的布局视图加入到AutoLayout布局体系中去的一些方法。
1.将布局视图添加到非布局父视图中
因为布局视图也是一个视图,都是从UIView派生。因此要将一个布局视图添加到采用AutoLayout约束的布局体系时,就像为普通视图一样给布局视图设置约束依赖即可。
2.使用布局视图的尺寸自适应属性
因为MyLayout&TangramKit中的布局视图具有设置尺寸自适应的属性,为了实现跟AutoLayout结合,最新版本的库的布局视图内部重载了intrinsicContentSize的方法。因此如果想使用布局视图的尺寸自适应功能,那么在将布局视图的尺寸设置为wrap后,就可以像使用UILabel那样不用去设置布局视图的宽度约束和高度约束了。比如有两个兄弟视图A,B。A视图是一个MyLayout&TangramKit布局视图,其宽度等于父视图S的宽度,而高度则根据布局视图里面的子视图的高度自适应,而B视图则在A视图的下方,并且宽度等于A视图。那么混合约束代码的实现如下:
//这里只演示OC代码。
MyLinearLayout *A = [MyLinearLayout new];
A.translatesAutoresizingMaskIntoConstraints = NO;
//这里设置A视图的高度自适应。
A.wrapContentHeight = YES;
[S addSubView:A];
//往A布局视图里面添加子视图的代码。。
UIView *B = [UIView new];
B.translatesAutoresizingMaskIntoConstraints = NO;
[S addSubView:B];
//A布局视图的约束设置,这里不需要设置高度约束,因为使用了布局视图的高度自适应属性。
[A.leftAnchor constraintEqualToAnchor:S.leftAnchor].active = YES;
[A.topAnchor constraintEqualToAnchor:S.topAnchor].active = YES;
[A.widthAnchor constraintEqualToAnchor:S.widthAnchor].active = YES;
//B视图必须设置完整约束
[B.leftAnchor constraintEqualToAnchor:S.leftAnchor].active = YES;
[B.topAnchor constraintEqualToAnchor:A.bottomAnchor].active = YES;
[B.widthAnchor constraintEqualToAnchor:A.widthAnchor].active = YES;
[B.heightAnchor constraintEqualToConstant:30].active = YES;
在布局库MyLayout中有更加复杂和详细的对布局视图如何和AutoLayout相互结合的代码:AllTest12ViewController。您可以在这个DEMO中看到如何实现父视图的尺寸和兄弟视图的尺寸和位置如何依赖尺寸自适应的布局视图的代码。
3.MyLayout&TangramKit的UITableViewCell高度自适应实现
如果你的所有视图都不使用AutoLayout的话则可以通过上面介绍的MyLayout&TangramKit来实现UITableViewCell的高度自适应的解决方案来实现。但是缺点就是要进行特定方法的重载。而这个问题在新版本中都已经得到解决了!!因为布局视图重载intrinsicContentSize方法,因此当将某个布局视图作为UITableViewCell的子视图时如果想使用布局视图的尺寸自适应的能力,只需要将布局视图的尺寸设置为wrap即可,然后将布局视图添加到其他视图中去,不需要再为布局视图设置宽度和高度约束了,也不再限制只能将布局视图添加到contentView中了,也不再需要重载特定的方法了,就相当于将一个布局视图当做UILabel视图来使用即可。具体的代码可以参考布局库MyLayout中的AllTest1TableViewCellForAutoLayout.m实现。
欢迎大家访问欧阳大哥2013的github地址