标签:AutoLayout iOS Swift Android 屏幕适配 界面布局 自动布局 UIView
TangramKit是iOS系统下用Swift编写的第三方界面布局框架。他集成了iOS的AutoLayout和SizeClass以及Android的五大容器布局体系以及HTML/CSS中的float和flex-box的布局功能和思想,目的是为iOS开发人员提供一套功能强大、多屏幕灵活适配、简单易用的UI布局解决方案。Tangram的中文即七巧板的意思,取名的寓意表明这个布局库可以非常灵巧和简单的解决各种复杂界面布局问题。他的同胞框架:MyLayout是一套用objective-C实现的界面布局框架。二者的主体思想相同,实现原理则是通过扩展UIView的属性,以及重载layoutSubviews方法来完成界面布局,只不过在一些语法和属性设置上略有一些差异。可以这么说TangramKit是MyLayout布局库的一个升级版本。大家可以通过访问下面的github站点去下载最新的版本:
在我10多年的开发生涯中,大部分时间都工作在客户端上。从DOS到Windows再到UNIX再到2010年接触iOS开发这6年多的时间中,总感觉一无所获,原因呢是觉没有什么积累。作为一个以编程为职业的人来说如果不留下什么可以值得为大家所知的东西的话,那将是一种职业上的遗憾。
就像每个领域都有工作细分一样,现在的编程人员也有明确分工:有一部分人做的是后端开发的工作,而有一部分人做的是前端开发的工作。二者相辅相成而完成了整个系统。后端开发的重点在于实现高性能和高可用,在数据处理上通常都是一个输入一个加工然后一个输出;而前端开发的重点在于实现界面流畅性和美观性,在数据处理上往往是多个输入一个加工和多个输出。在技术层面上后端处理的对象是多线程多进程以及数据,而前端处理的对象则是图形绘制和以及界面布局和动画特效。
这篇文章的重点是介绍界面布局的核心,因此其他部分就不再展开去说了。对于一个UI界面来说,好的界面布局体系往往能起到事半工倍的作用。PC设备上因为屏幕总是够大,比如VB,VF,PB,Dephi,AWT,Swing等语言或者环境下的应用开发非常方便,IDE环境中提供一个所见即所得的开发面板(form),人们只要使用简单的拖拉拽动作就可把各种界面元素加入到form中就可以形成一个小程序了。而开发VC程序则相对麻烦,系统的IDE环境对可视化编程的支持没有那么的完善,因此大部分界面的构建都需要通过编码来完成。同时因为PC设备屏幕较大而且标准统一,因此几乎不存在界面要在各种屏幕尺寸适配的问题。唯一引起争议是可视化编程和纯代码编程的方式之争,这种争议也体现在iOS应用的开发身上,那就是用XIB和SB以及纯代码编写界面的好坏争议。关于这个问题个人的意见是各有各好:XIB/SB进行布局时容易上手且所见即所得,但缺乏灵活性和可定制化;而纯代码则灵活性高可定制化强,缺点是不能所见即所得和代码维护以及系统分层模糊。
再回到屏幕适配的话题来说,如果说PC时代编程屏幕尺寸适配不是很重要的工作,那么到了移动设备时代则不一样了,适配往往成为整个工作的重点和难点。主要的原因是设备的屏幕尺寸和设备分辨率的多样性的差异,而且要求在这么小的屏幕上布局众多的要素,同时又要求界面美观和友好的用户体验,这就非常考验产品以及UI/UE人员和开发人员的水平,同时这部分工作也占用了开发者的大部分时间。在现有的两个主流的移动平台上,Android系统因为本身硬件平台差异性的原因,为了解决这些差异性而设计了一套非常方便的和友好的界面布局体系。它提出了布局容器的概念,也就是有专门职责的布局容器视图来管理和排列里面的子视图,根据实际中的应用场景而把这些负责布局的容器视图分类抽象出了线性布局、相对布局、框架布局、表格布局、绝对布局这5大容器布局,而这些也就构成了Android系统布局体系的核心实现。也正是这套布局机制使得Android系统能够方便的胜任多种屏幕尺寸和分辨率在不同硬件设备上的UI界面展示。而对于iOS的开发人员来说,早期的设备只有单一的3.5in大小且分辨率也只有480x320和960x640这两种类型的设备,因此开发人员只需要采用绝对定位的方式通过视图的frame属性设置来实现界面的布局,根本不需要考虑到屏幕的适配问题。但是这一切从苹果后续依次发布iPhone4/5/6/7系列的设备后被打破了,整个iOS应用的开发也需要考虑到多屏幕尺寸和多分辨率的问题了,这样原始的frame方法进行布局设置将不能满足这些多屏幕的适配问题了,因此iOS提出了一套新的界面布局体系:AutoLayout以及SizeClass. 这套机制通过设置视图之间的位置和尺寸的约束以及对屏幕尺寸进行分类的方式来完成界面的布局和屏幕的适配工作。
尽管如此, 虽然两个移动端平台都提供了自己独有且丰富的界面布局体系,但对于移动客户端开发人员来说界面布局和适配仍然是我们在开发中需要重点关注的因素之一。
我们知道,在界面开发中我们直接操作的对象是视图,视图可以理解为一个具有特定功能的矩形区块,因此所谓的布局的本质就是为视图指定某个具体的尺寸以及指定其排列在屏幕上的位置。因此布局的动作就分为两个方面:一个是指定视图的尺寸,一个是指定视图的位置。
视图的尺寸就是指视图矩形块的大小,为了表征视图的大小我们称在屏幕水平方向的尺寸大小为宽度,而称在屏幕垂直方向的尺寸大小为高度,因此一个视图的尺寸我们就可以用宽度和高度两个维度的值来描述了,宽度和高度的单位我们称之为点。UIView中用bounds属性的size部分来描述视图的尺寸(bounds属性的origin部分后面会介绍到)。 对于屏幕尺寸来说同样也用宽度和高度来描述。在视图层次体系结构中的顶层视图的尺寸和屏幕的尺寸是一致的,为了描述这个特殊的顶层视图我们将这个顶层根视图称之为窗口,窗口的尺寸和屏幕的尺寸一样大,同时窗口是一切视图的容器视图。一个视图的尺寸我们可以用一个具体的数值来描述,比如某个视图的宽度和高度分别为:100x200。我们称这种定义的方式为绝对值类型的尺寸。但是在实际中我们的一些视图的尺寸并不能够一开始就被明确,原因是这些视图的尺寸大小和其他视图的尺寸大小有关,也就是说视图的尺寸依赖于另外一个视图或者另外一组视图。比如说有A和B两个视图,我们定义A视图的宽度和B视图的宽度相等,而A视图的高度则是B视图高度的一半。也就是可以表述为如下:
A.bounds.size.width = B.bounds.size.width A.bounds.size.height = B.bounds.size.height /2 //父视图S的高度等于里面子视图A,B的高度的总和
S.bounds.size.height = A.bounds.size.height + B.bounds.size.height
我们称为这种尺寸的定义方式为相对值类型的尺寸。在相对值类型的尺寸中, 视图某个维度的尺寸所依赖的另外一个视图可以是它的兄弟视图,也可以是它的父视图,也可以是它的子视图,甚至可以是它自身的其他维度。 这种视图尺寸的依赖关系是可以传递和递归的,比如A依赖于B,而B右依赖于C。 但是这种递归和传递关系不能形成一个闭环依赖,也就是说在依赖关系的最终节点视图的尺寸的值必须是一个绝对值类型或者特定的相对值类型(wrap包裹值),否则的话我们将形成约束冲突而进入死循环的场景。
视图的尺寸之间的依赖关系还有两种特定的场景:
可以看出包裹和填充尺寸是相对值类型中的两种特殊的类型,他所依赖的视图并不是某个具体的视图,而是一些相关的视图的集合。
为了表征视图的尺寸以及尺寸可以设置的值的类型,我们就需要对尺寸进行建模,在TangramKit框架中TGLayoutSize类就是一个尺寸类,这个类里面的equal方法则是用来设置视图尺寸的各种类型的值:包括绝对值类型,相对值类型,以及包裹和填充的值类型等等。同时我们对UIView扩展出了两个属性tg_width, tg_height分别用来表示视图的布局宽度和布局高度。他其实是对原生的视图bounds属性中的size部分进行了扩充和延展。原始的bounds属性中的size部分只能设置绝对值类型的尺寸,而不能设置相对值类型的尺寸。
当一个视图的尺寸确定后,接下来我们就需要确定视图所在的位置了。所谓位置就是指视图在屏幕中的坐标位置,屏幕中的坐标分为水平坐标也就是x轴坐标,和垂直坐标也就是y轴坐标。而这个坐标原点在不同的系统中有区别:iOS系统采用左手坐标系,原点都是在左上角,并且规定y轴在原点以下是正坐标轴,而原点以上是负坐标轴,而x轴则在原点右边是正坐标轴,原点左边是负坐标轴。OSX系统则采用右手坐标系,原点在左下角,并且规定y轴在原点以上是正坐标轴,而在原点以下是负坐标轴,而x轴则在原点右边是正坐标轴,原点左边是负坐标轴。
因此视图位置的确定我们需要考虑两个方面的问题:一个是位置是相对于哪个坐标系?一个是视图内部的哪个部位来描述这个位置?
确定一个视图的位置时总是应该有一个参照物,在现有的布局体系中一般分为三种参照物:屏幕、父视图、兄弟视图。
* 第一种以屏幕坐标系作为参照来确定的位置称为绝对位置,也就是以屏幕的左上角作为原点,每个视图的位置都是距离屏幕左上角原点的一个偏移值。这种绝对位置的设置方式的优点是所有视图的参照物都是一致的,便于比较和计算,但缺点是对于那些多层次结构的视图以及带滚动效果的视图来说位置的确定则总是需要进行动态的变化和计算。比如某个滚动视图内的所有子视图在滚动时都需要重新去计算自己的位置。
上面的三种定位方式各有优缺点,我们可以在实际中结合各种定位方式来完成视图的位置设定。
上面我们介绍了定位时位置所基于的坐标系,因为视图并不是一个点而是一个矩形区块,所以我们必须要明确的是视图本身这个区块的哪个点来进行位置的设定。 在这里我们就要介绍视图内的坐标系。我们知道视图是一个矩形的区域,里面由无数个点构成。假如我们以视图左上角作为坐标原点的话,那么视图内的任何一点都可以用水平方向的坐标值和垂直方向的坐标值来表示。对于水平方向的坐标值来说最左边位置的点的坐标值是0,最右边位置的点的坐标值是视图的宽度,中间位置的坐标点的值是宽度的一半,对于垂直方向的坐标值来说最上边位置的点的坐标值是0,最下边位置的点的坐标值是视图的高度,中间位置的坐标点的值是高度的一半。我们称这几个特殊的坐标点为方位。因此一个视图一共有9个方位点分别是:左上、左中、左下、中上、中中、中下、右上、右中、右下。
通过对方位点的定义,我们就不再需要去关心这些点的具体的坐标值了,因为他描述了视图的某个特定的部位。而为了方便计算和处理,我们一般只需要指出视图内某个方位点在参照视图的坐标系里面的水平坐标轴和垂直坐标轴中的位置就可以完成视图的位置定位了,因为只要确定了这个方位点的在参照视图坐标系里面的位置,就可以计算出这个视图内的任意的一个点在参照视图坐标轴里面的位置。所谓的位置定位就是把一个视图内坐标系的某个点的坐标值映射为参照视图坐标系里面的坐标值的过程。
iOS中UIView提供了一个属性center,center属性的意义就是定义视图内中心点这个方位在父视图坐标系中的坐标值。我们再来考察一下UIView的bounds属性,上面的章节中我们有介绍bounds中的size部分用来描述一个视图的尺寸,而origin部分又是用来描述什么呢? 我们知道在左手坐标系里面,一个视图内的左上角方位的坐标值就是原点的坐标值,默认情况下原点的坐标值是(0,0)。但是这个定义不是一成不变的,也就是说原点的坐标值不一定是(0,0)。一个视图bounds里面的origin部分所表达的意义就是视图内左上角的坐标值,size部分所表达的意义就是视图本身的尺寸。这样我们就可以通过下面的公式得出一个视图内9个方位(再次强调方位的概念是一个视图内的坐标点的位置)的坐标值:
左上方位 = (A.bounds.origin.x, A.bounds.origin.y)
左中方位 = (A.bounds.origin.x, A.bounds.origin.y + A.bounds.size.height / 2)
左下方位 = (A.bounds.origin.x, A.bounds.origin.y + A.bounds.size.height)
中上方位 = (A.bounds.origin.x + A.bounds.size.width/2, A.bounds.origin.y)
中中方位 = (A.bounds.origin.x + A.bounds.size.width/2, A.bounds.origin.y + A.bounds.size.height/2)
中下方位 = (A.bounds.origin.x + A.bounds.size.width/2, A.bounds.origin.y + A.bounds.size.height)
右上方位 = (A.bounds.origin.x + A.bounds.size.width, A.bounds.origin.y)
右中方位 = (A.bounds.origin.x + A.bounds.size.width,A.bounds.origin.y + A.bounds.size.height/2)
右下方位 = (A.bounds.origin.x + A.bounds.size.width,A.bounds.origin.y + A.bounds.size.height)
对于位置定义来说TangramKit中的TGLayoutPos类就是一个对位置进行建模的类。TGLayoutPos类同时支持采用父视图作为参考系和以兄弟视图作为参考系的定位方式,这可以通过为其中的equal方法设置不同类型的值来决定其定位方式。为了实现视图定位我们也为UIView扩展出了3个水平方位的属性:tg_left, tg_centerX,tg_right来表示左中右三个方位对象。3垂直方位的属性:tg_top, tg_centerY,tg_bottom来表示上、中、下三个方位。这6个方位对象将比原生的center属性提供更加强大和丰富的位置定位能力。
iOS系统的原生布局体系里面是通过bounds属性和center属性来进行视图的尺寸设置和位置设置的。bounds用来指定视图内的左上角方位的坐标值,以及视图的尺寸,而center则用来指定视图的中心点方位在父视图这个坐标体系里面的坐标值。为了简化设置UIView提供了一个简易的属性frame可以用来直接设置一个视图的尺寸和位置,frame中的origin部分指定视图左上角方位在父视图坐标系里面的坐标值,而size部分则指定了视图本身的尺寸。frame属性并不是一个实体属性而是一个计算类型的属性,在我们没有对视图进行坐标变换时(视图的transform未设置时)我们可以得到如下的frame属性的伪代码实现:
public var frame:CGRect
{
get {
let x = self.center.x - self.bounds.size.width / 2 let y = self.center.y - self.bounds.size.height / 2 let width = self.bounds.size.width let height = self.bounds.size.height return CGRect(x:x, y:y, width:width, height:height)
}
set {
self.center = CGPoint(x:newValue.origin.x + newValue.size.width / 2, y: newValue.origin.y + newValue.size.height / 2)
self.bounds.size = newValue.size }
}
综上所述,我们可以看出,所谓视图布局的核心,就是确定一个视图的尺寸,和确定视图在参考视图坐标系里面的坐标位置。为了灵活处理和计算,视图的尺寸可以设置为绝对值类型,也可以设置为相对值类型,也可以设置为特殊的包裹或者填充值类型;视图的位置则可以指定视图中的任意的方位,以及设置这个方位的点在窗口坐标系或者父视图坐标系或者兄弟坐标系中的坐标值。正是提供的这些多样的设置方式,我们就可以在不同的场景中使用不同的设置来完成各种复杂界面的布局。
在您不了解TangramKit之前,可以先通过下面一个例子来感受和体验一下TangramKit的布局构建语法:
- 有一个容器视图S的宽度是100而高度则等于由四个从上到下依次排列的子视图A,B,C,D的高度总和。
- 子视图A的左边距占用父视图宽度的20%,而右边距则占用父视图宽度的30%,高度则等于自身的宽度。
- 子视图B的左边距是40,宽度则占用父视图的剩余宽度,高度是40。
- 子视图C的宽度占用父视图的所有宽度,高度是40。
- 子视图D的右边距是20,宽度是父视图宽度的50%,高度是40。
代码实现如下:
let S = TGLinearLayout(.vert)
S.tg_vspace = 10 S.tg_width.equal(100)
S.tg_height.equal(.wrap)
let A = UIView()
A.tg_left.equal(20%)
A.tg_right.equal(30%)
A.tg_height.equal(A.tg_width)
S.addSubview(A)
let B = UIView()
B.tg_left.equal(40)
B.tg_width.equal(.fill)
B.tg_height.equal(40)
S.addSubview(B)
let C = UIView()
C.tg_width.equal(.fill)
C.tg_height.equal(40)
S.addSubview(C)
let D = UIView()
D.tg_right.equal(20)
D.tg_width.equal(50%)
D.tg_height.equal(40)
S.addSubview(D)
因为TangramKit对布局位置类和布局尺寸类的方法重载了运算符:~=、>=、<=、+=、-=、*=、/=所以您可以用更加简洁的代码进行编写:
let S = TGLinearLayout(.vert)
S.tg_vspace = 10 S.tg_width ~=100 S.tg_height ~=.wrap let A = UIView()
A.tg_left ~=20%
A.tg_right ~=30%
A.tg_height ~=A.tg_width
S.addSubview(A)
let B = UIView()
B.tg_left ~=40 B.tg_width ~=.fill B.tg_height ~=40 S.addSubview(B)
let C = UIView()
C.tg_width ~=.fill C.tg_height ~=40 S.addSubview(C)
let D = UIView()
D.tg_right ~=20 D.tg_width ~=50%
D.tg_height ~=40 S.addSubview(D)
通过上面的代码,您可以看出用TangramKit实现的布局代码和上面场景描述文本几乎相同,非常的利于阅读和理解。那么这些系统又是如何实现的呢?
我们知道在对任何一个视图进行布局时,最终都是通过设置视图的尺寸和视图的位置来完成的。在iOS中我们可以通过UIView的bounds属性来完成视图的尺寸设置,而通过center属性来完成视图的位置设置。为了进行简单的操作,系统提供了frame这个属性来简化对尺寸和位置的设置。这个过程不管是原始的方法还是后续的AutoLayout其实现的最终机制都是一致的。每当一个视图的尺寸改变或者要求重新布局时,系统都会调用视图的方法:
open func layoutSubviews()
而我们可以在UIView的派生类中重载上面的方法来实现对这个视图里面的所有子视图的重新布局,至于如何布局子视图则是需要根据应用场景而定。在编程时我们经常会用到一些视图,这种视图只是负责将里面的子视图按照某种规则进行排列和布局,而别无其他的作用。因此我们称这种视图为容器视图或者称为布局视图。TangramKit框架对种视图进行了建模而提供了一个从UIView派生的布局视图基类TGBaseLayout。这个类的作用就是专门负责对加入到其中的所有子视图进行布局排列,它是通过重载layoutSubviews方法来完成这个工作的。刚才我们说过如何排列容器视图中的子视图是要根据具体的应用场景而定, 比如有可能是所有子视图从上往下按照添加的顺序依次排列,或者子视图按照某种约束依赖关系来进行布局排列,或者子视图需要多行多列的排列等等。因此我们对常见的布局应用场景进行了抽象,通过建立不同的TGBaseLayout的派生类来实现不同的布局处理:
* 线性布局TGLinearLayout:线性布局里面的所有子视图都按照添加的顺序依次从上到下或者依次从左到右进行排列。根据排列的方向可以分为垂直线性布局和水平线性布局。线性布局和iOS9上的UIStackView以及Android中的线性布局LinearLayout提供一样的功能。
* 框架布局TGFrameLayout: 框架布局里面的所有子视图布局时和添加的顺序无关,而是按照设定的位置停靠在布局视图的:左上、左中、左下、中上、中中、中下、右上、右中、右下、填充这个10个方位中的任何一个位置上。框架布局里面的子视图只跟框架布局视图的边界建立约束关系。框架布局和Android中的框架布局FrameLayout提供一样的功能。
* 表格布局TGTableLayout:表格布局里面的子视图可以进行多行多列的排列。在使用时要先添加行,然后再在行里面添加列,每行的列数可以随意确定。因为表格布局是线性布局TGLinearLayout的派生类,所以表格布局也分为垂直表格布局和水平表格布局。垂直表格布局中的行是从上到下,而列则是从左到右排列;水平表格布局中的行是从左到右,而列是从上到下排列的。表格布局和Android中的表格布局TableLayout以及HTML中的table,tr,td元素提供一样的功能。
* 相对布局TGRelativeLayout: 相对布局里面的子视图和添加的顺序无关,而是按照子视图之间设定的尺寸约束依赖和位置约束依赖进行布局排列。因此相对布局里面的所有子视图都要设置位置和尺寸的约束和依赖关系。相对布局和iOS的AutoLayout以及Android中的相对布局RelativeLayout提供一样的功能。
* 流式布局TGFlowLayout: 流式布局里面的子视图按照添加的顺序依次从某个方向排列,而当遇到了这个方向上的排列数量限制或者容器的尺寸限制后将会另起一行,而重新按照原先的方向依次排列。最终这个布局中的子视图将形成多行多列的排列展示。流式布局和线性布局的区别是,线性布局只是单行或者单列的,而流式布局则是多行多列。流式布局和表格布局的区别是,表格布局有明确行的概念,在使用前要添加行再添加列,而流式布局则没有明确行的概念,由布局自动生成行和列。根据排列的方向和限制的规则,流式布局分为垂直数量约束布局、垂直内容约束布局、水平数量约束布局、水平内容约束布局四种布局。流式布局实现了HTML/CSS3中的flex-box的子集的功能。
上述的7个派生类分别的实现了大部分的不同的应用场景。在每个派生类的layoutSubviews的实现中都按照描述的规则来设置子视图的尺寸bounds和位置center属性。也就是说最终的子视图的尺寸和位置是在布局视图中的layoutSubviews中进行设置的。那么我们就必须要提供另外一套子视图的布局尺寸和布局位置的设置方法,以便在布局视图布局时将子视图设置好的布局尺寸和布局位置转化为真实的视图尺寸和视图位置。为此TangramKit专门提供了一个视图的布局尺寸类TGLayoutSize用来进行子视图的布局尺寸的设置,一个视图的布局位置类TGLayoutPos用来进行子视图的布局位置的设置。我们对UIView建立了一个extension。分别扩展出了2个布局尺寸对象和6个布局位置对象:
extension UIView
{ //左边位置 var tg_left:TGLayoutPos{get} //上边位置 var tg_top:TGLayoutPos{get} //右边位置 var tg_right:TGLayoutPos{get} //下边位置 var tg_bottom:TGLayoutPos{get} //水平中心点位置 var tg_centerX:TGLayoutPos{get} //垂直中心点位置 var tg_centerY:TGLayoutPos{get} //宽度尺寸 var tg_width:TGLayoutSize{get} //高度尺寸 var tg_height:TGLayoutSize{get}
}
也就是说我们将不再直接设置子视图的bounds和center(这两个属性只会在布局视图中的layoutSubviews中设置)属性了,而是直接操作UIView扩展出来的布局位置对象和布局尺寸对象。如果把布局视图的layoutSubviews比作一个数学函数的话,那么我们就能得到如下的方程式:
UIView.center = TGXXXLayout.layoutSubviews(UIView.tg_left, UIView.tg_top, UIView.tg_right, UIView.tg_bottom,UIView.tg_centerX,UIView.tg_centerY)
UIView.bounds = TGXXXLayout.layoutSubviews(UIView.tg_width, UIView.tg_height)
因此我们可以看出不同的TGBaseLayout的派生类因为里面的布局方法不相同,而导致子视图的位置和尺寸的计算方法不同,从而得到了我们想要的效果。那么为什么要用6个布局位置对象和2个布局尺寸对象来设置子视图的位置和尺寸而不直接用bounds和center呢? 原因在于bounds和center只提供了有限的设置方法而布局位置对象和布局尺寸对象则提供了功能更加强大的设置方法,而这些方法又可以简化我们的编程,以及可以很方便的适配各种不同尺寸的屏幕。(还记得我们上面的例子里面,尺寸和位置可以设置为数值,.wrap, .fill,以及百分比的值吗?)。
TangramKit为了存储这些扩展的布局位置和布局尺寸对象,内部是使用了objc的runtime机制提供的动态属性创建的方法:
public func objc_getAssociatedObject(_ object: Any!, _ key: UnsafeRawPointer!) -> Any!
系统通过这个方法来关联视图对象的那6个布局位置和2个布局尺寸对象。
上面的代码中我们看到了布局容器视图通过layoutSubviews方法来实现对子视图的重新布局。而且也提到了当容器视图的尺寸发生变化时也会激发对layoutSubviews的调用。除了自动激发外,我们可以通过手动调用布局视图的setNeedLayout方法来实现布局视图的layoutSubviews调用。当我们在设置子视图的布局位置和布局尺寸时,系统内部会在设置完成后调用布局视图的setNeedLayout的方法,因此只要对子视图的布局位置和布局尺寸进行设置都会重新激发布局视图的布局视图。那么对子视图的frame,bounds,center真实位置和尺寸的改变呢?我们也要激发布局视图的重新布局。为了解决这个问题,我们引入了KVO的机制。布局视图在添加子视图时会监听加入到其中的子视图的frame,bounds,center的变化,并在其变化时调用布局视图的setNeedLayout来激发布局视图的重新布局。我们知道每次当一个视图调用addSubview添加子视图时都会激发调用者的方法:didAddSubview。为了实现对子视图的变化的监控,布局视图重载了这个方法并对子视图的isHidden,frame,center进行监控:
override open func didAddSubview(_ subview: UIView) { super.didAddSubview(subview)
subview.addObserver(self, forKeyPath:"isHidden", options: NSKeyValueObservingOptions.new, context: nil)
subview.addObserver(self, forKeyPath:"frame", options: NSKeyValueObservingOptions.new, context: nil)
subview.addObserver(self, forKeyPath:"center", options: NSKeyValueObservingOptions.new, context: nil)
}
override open func willRemoveSubview(_ subview: UIView) { super.willRemoveSubview(subview)
subview.removeObserver(self, forKeyPath: "isHidden")
subview.removeObserver(self, forKeyPath: "frame")
subview.removeObserver(self, forKeyPath: "center")
}
当子视图的frame或者center变更时,将会激发布局视图的重新布局。上面曾经说过,在布局视图重新布局子视图时最终会调整子视图的bounds和center.那么这样就有可能会形成循环的重新布局,为了解决这种循环递归的情况,布局视图在layoutSubviews调用进行布局前设置了一个布局中的标志,而在所有子视图布局完成后将恢复这个布局中的标志。因此当我们布局视图通过KVO监控到子视图的位置和尺寸变化时,则会判断那个布局中的标志,如果当前是在布局中则不会再次激发布局视图的重新布局,从而防止了死循环的发生。
这就是TangramKit布局实现的原理,下面的图表列出了TangramKit的整个布局框架的类体系结构:
在前面的介绍布局核心的章节以及布局实现原理的章节里面我们有说道布局位置类和布局尺寸类。之所以系统不直接操作视图的bounds和center属性而是通过扩展视图的2个布局尺寸属性和6个布局位置属性来进行子视图的布局设置。原因是后者能够提供丰富和多样的设置。而且我们在编程时也不再需要通过设置视图的frame来实现布局了,即使设置也可能会失效。
TGWeight类的值表示尺寸或者位置的大小是父布局视图的尺寸或者剩余空间的尺寸的比例值,也就是说值的大小依赖于父布局视图的尺寸或者剩余空间的尺寸的大小而确定,这样子视图就不需要明确的指定位置和尺寸的大小了,非常适合那些需要适配屏幕的尺寸和位置的场景。 至于是父视图的尺寸还是父视图剩余空间的尺寸则要根据其所在的布局视图的上下文而确定。
布局尺寸类用来描述视图布局核心中的视图尺寸。我们对UIView扩展出了2个布局尺寸对象 :
public var tg_width:TGLayoutSize public var tg_height:TGLayoutSize
分别用来实现视图的宽度和高度的布局尺寸设置。在TGLayoutSize类中,我们可以通过方法equal来设置视图尺寸的多种类型的值:
特殊类型的值。为了简化尺寸的设置我们定义了三种特殊类型的尺寸值:
wrap: 他表示尺寸的值由布局视图的所有子视图的尺寸或者由子视图的内容包裹而成。也就是尺寸的大小是由子视图或者视图的内容共同决定的,这样视图的尺寸将依赖其内部的子视图的尺寸或者子视图内容的大小。
fill: 他表示视图的尺寸的值将会填充满父视图的剩余空间,也就是说视图的尺寸值是依赖于父视图的尺寸的大小。
average:他表示视图的尺寸将和其兄弟视图一起来均分父视图的尺寸,这样所有兄弟视图的尺寸都将相等。
在布局尺寸类中我们除了可以用equal, add, multiply方法来设置视图的尺寸依赖值以及增量和倍数外,我们还可以对视图尺寸的最大最小值进行控制处理。比如在实践中我们希望某个视图的宽度等于另外一个兄弟视图的宽度,但是最小不能小于20,而最大则不能超过父视图的宽度的一半。 这时候我们就需要用到布局尺寸类的另外两个方法min,max分别用来设置视图尺寸最小不能小于的值以及最大不能超过的值。方法中我们可以看出最大最小值除了可以设置具体的数值外还可以设置为另外一个布局尺寸对象,同样我们还可以设置增量和倍数值。
布局位置类用来描述视图布局核心中的视图的位置。我们对UIView扩展出了6个布局位置对象:
public var tg_left:TGLayoutPos //视图左边布局位置 public var tg_top:TGLayoutPos //视图上边布局位置 public var tg_right:TGLayoutPos //视图右边布局位置 public var tg_bottom:TGLayoutPos //视图下边布局位置 public var tg_centerX:TGLayoutPos //视图水平中心点布局位置 public var tg_centerY:TGLayoutPos //视图垂直中心点布局位置
分别用来实现视图的水平维度的左、中、右三个方位以及视图垂直维度的上、中、下三个方位的布局位置设置。在TGLayoutPos类中,我们可以通过方法equal来设置视图位置的多种类型的值
对于绝对值类型的位置值,他所表示的意义是边距还是间距这个要看他所加入的布局视图的类型而不同。
//A的左边距等于父视图的宽度的20%,最小为20,最大为30 A.tg_left.equal(20%).min(20).max(30)