V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
newkengsir
V2EX  ›  iOS

OC/Swift 实现的功能强大的界面布局框架

  •  
  •   newkengsir · 2016-11-29 10:13:25 +08:00 · 5261 次点击
    这是一个创建于 2951 天前的主题,其中的信息可能已经有所发展或是发生改变。

    由于字数限制,本篇介绍文有所删减,详细可前往介绍地址查看详细

    OC 版本介绍地址: http://www.code4app.com/thread-7501-1-1.html

    Swift 版本介绍地址: http://www.code4app.com/thread-11767-1-1.html

    ##前言

    ***TangramKit是 iOS 系统下用 Swift 编写的第三方界面布局框架。他集成了 iOS 的 AutoLayout 和 SizeClass 以及 Android 的五大容器布局体系以及 HTML/CSS 中的 float 和 flex-box 的布局功能和思想,目的是为 iOS 开发人员提供一套功能强大、多屏幕灵活适配、简单易用的 UI 布局解决方案。 Tangram 的中文即七巧板的意思,取名的寓意表明这个布局库可以非常灵巧和简单的解决各种复杂界面布局问题。他的同胞框架:MyLayout***是一套用 objective-C 实现的界面布局框架。二者的主体思想相同,实现原理则是通过扩展 UIView 的属性,以及重载layoutSubviews方法来完成界面布局,只不过在一些语法和属性设置上略有一些差异。可以这么说 TangramKit 是 MyLayout 布局库的一个升级版本。大家可以通过访问下面的 github 站点去下载最新的版本:

    TangramKit 演示效果图

    ##所见即所得和编码之争以及屏幕的适配 在我 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 包裹值),否则的话我们将形成约束冲突而进入死循环的场景。

    两种尺寸约束依赖

    视图的尺寸之间的依赖关系还有两种特定的场景:

    • 某个视图的尺寸依赖于里面所有子视图的尺寸的大小或者依赖于视图内所展示的内容的尺寸,我们称这种依赖为**包裹(wrap)**。
    • 某个视图的尺寸依赖于所在父视图的尺寸减去其他兄弟视图所占用的尺寸的剩余尺寸也就是说尺寸等于父视图的尺寸和其兄弟视图尺寸的差集,我们称这种依赖为**填充(fill)**。

    可以看出包裹和填充尺寸是相对值类型中的两种特殊的类型,他所依赖的视图并不是某个具体的视图,而是一些相关的视图的集合。

    为了表征视图的尺寸以及尺寸可以设置的值的类型,我们就需要对尺寸进行建模,在 TangramKit 框架中**TGLayoutSize**类就是一个尺寸类,这个类里面的 equal 方法则是用来设置视图尺寸的各种类型的值:包括绝对值类型,相对值类型,以及包裹和填充的值类型等等。同时我们对 UIView 扩展出了两个属性tg_width, tg_height分别用来表示视图的布局宽度和布局高度。他其实是对原生的视图bounds属性中的 size 部分进行了扩充和延展。原始的bounds属性中的 size 部分只能设置绝对值类型的尺寸,而不能设置相对值类型的尺寸。

    视图的位置

    当一个视图的尺寸确定后,接下来我们就需要确定视图所在的位置了。所谓位置就是指视图在屏幕中的坐标位置,屏幕中的坐标分为水平坐标也就是 x 轴坐标,和垂直坐标也就是 y 轴坐标。而这个坐标原点在不同的系统中有区别: iOS 系统采用左手坐标系,原点都是在左上角,并且规定 y 轴在原点以下是正坐标轴,而原点以上是负坐标轴,而 x 轴则在原点右边是正坐标轴,原点左边是负坐标轴。 OSX 系统则采用右手坐标系,原点在左下角,并且规定 y 轴在原点以上是正坐标轴,而在原点以下是负坐标轴,而 x 轴则在原点右边是正坐标轴,原点左边是负坐标轴。

    不同的坐标系

    因此视图位置的确定我们需要考虑两个方面的问题:一个是位置是相对于哪个坐标系?一个是视图内部的哪个部位来描述这个位置?

    确定一个视图的位置时总是应该有一个参照物,在现有的布局体系中一般分为三种参照物:屏幕、父视图、兄弟视图

    • 第一种以屏幕坐标系作为参照来确定的位置称为绝对位置,也就是以屏幕的左上角作为原点,每个视图的位置都是距离屏幕左上角原点的一个偏移值。这种绝对位置的设置方式的优点是所有视图的参照物都是一致的,便于比较和计算,但缺点是对于那些多层次结构的视图以及带滚动效果的视图来说位置的确定则总是需要进行动态的变化和计算。比如某个滚动视图内的所有子视图在滚动时都需要重新去计算自己的位置。

    • 第二种以父视图坐标系作为参照来确定的位置称为相对位置,每个子视图的位置都是距离父视图左上角原点的一个偏移值。这样的好处就是每个子视图都不再需要关心屏幕的原点,而只需要以自己的父视图为原点进行位置的计算就可以了,这种方式是目前大部分布局体系里面采用的定位方式,也是最方便的定位方式,缺点是不同层次之间的视图的位置在进行比较时需要一步步的往上进行转换,直到转换到在窗口中的位置为止。我们称这种以父视图坐标系为原点进行定位的位置称为边距,也就是离父视图边缘的距离。

    • 第三种以兄弟视图坐标系作为参照来确定的位置称为偏移位置,子视图的位置是在关联的兄弟视图的位置的基础之上的一个偏移值。比如 A 视图在 B 视图的右边偏移 5 个点,则表示为 A 视图的左边距离 B 视图的右边 5 个点的距离。我们称这种坐标体系下的位置为间距,也就是指定的是视图之间的距离作为视图的位置。采用间距的方式进行定位只适合于同一个父视图之间的兄弟视图之间的定位方式。 各种坐标系下的定位值

    上面的三种定位方式各有优缺点,我们可以在实际中结合各种定位方式来完成视图的位置设定。

    上面我们介绍了定位时位置所基于的坐标系,因为视图并不是一个点而是一个矩形区块,所以我们必须要明确的是视图本身这个区块的哪个点来进行位置的设定。 在这里我们就要介绍视图内的坐标系。我们知道视图是一个矩形的区域,里面由无数个点构成。假如我们以视图左上角作为坐标原点的话,那么视图内的任何一点都可以用水平方向的坐标值和垂直方向的坐标值来表示。对于水平方向的坐标值来说最左边位置的点的坐标值是 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 之前,可以先通过下面一个例子来感受和体验一下 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 的子集的功能。

    • 浮动布局 TGFloatLayout :浮动布局里面的子视图按照添加的顺序,并且按照每个子视图自身设定的浮动规则向某个方向进行浮动停靠。当子视图的尺寸无法容纳到布局视图的剩余空间时,则会自动寻找一个能够容纳自身尺寸的最佳位置进行浮动停靠。浮动布局里面的子视图并不是有规则的多行多列的排列。根据子视图可以浮动的方向浮动布局分为垂直浮动布局和水平浮动布局。浮动布局和 HTML/CSS 中的 float 定位实现了相同的功能。

    • 路径布局 TGPathLayout: 路径布局里面的子视图按照一个提供的数学函数得到的曲线路径等距离的根据添加的顺序依次排列。所有的子视图的位置都是根据函数曲线中距离相等的点而确定的。路径布局提供了直角坐标系、参数方式、极坐标系三种曲线的构建方法。路径布局是 TangramKit 中的独有的一种布局。

    上述的 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}
    }
    
    

    也就是说我们将不再直接设置子视图的boundscenter(这两个属性只会在布局视图中的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 个布局尺寸对象来设置子视图的位置和尺寸而不直接用boundscenter呢? 原因在于 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 的整个布局框架的类体系结构:

    TangramKit 布局框架体系架构

    ###布局位置类和布局尺寸类 在前面的介绍布局核心的章节以及布局实现原理的章节里面我们有说道布局位置类和布局尺寸类。之所以系统不直接操作视图的bounds 和 center属性而是通过扩展视图的 2 个布局尺寸属性和 6 个布局位置属性来进行子视图的布局设置。原因是后者能够提供丰富和多样的设置。而且我们在编程时也不再需要通过设置视图的 frame 来实现布局了,即使设置也可能会失效。

    ####比重类 TGWeight TGWeight 类的值表示尺寸或者位置的大小是父布局视图的尺寸或者剩余空间的尺寸的比例值,也就是说值的大小依赖于父布局视图的尺寸或者剩余空间的尺寸的大小而确定,这样子视图就不需要明确的指定位置和尺寸的大小了,非常适合那些需要适配屏幕的尺寸和位置的场景。 至于是父视图的尺寸还是父视图剩余空间的尺寸则要根据其所在的布局视图的上下文而确定。比如:

    //假如 A,b 是在一个垂直线性布局下的子视图
    A.tg_width.equal(TGWeight(20))   //A 的宽度是父布局视图宽度的 20%
    A.tg_height.equal(TGWeight(30))  //A 的高度是父布局视图剩余高度的 30%
    B.tg_left.equal(TGWeight(40))  //B 的左边距是父视图宽度的 40%
    B.tg_top.equal(TGWeight(10))  //B 的顶部间距时父视图的剩余高度的 10%
    

    为了简化和更加直观的表示比重类型的值,我们重载%运算符,这样上面的代码就可以简写为如下更加直观的方式:

    //假如 A 是在一个垂直线性布局下的子视图
    A.tg_width.equal(20%)   //A 的宽度是父布局视图宽度的 20%
    A.tg_height.equal(30%)  //A 的高度是父布局视图剩余高度的 30%
    B.tg_left.equal(40%)  //B 的左边距是父视图宽度的 40%
    B.tg_top.equal(10%)  //B 的顶部间距时父视图的剩余高度的 10%
    

    下面的列表中列出了在各种布局下视图的尺寸和位置的 TGWeight 类型值所代表的意义:

    为了表示方便,我们把:

    • 线性布局简称 L
      • 垂直线性布局简称为 LV
      • 水平线性布局简称为 LH
    • 框架布局简称为 FR
    • 垂直表格布局简称为 TV
    • 水平表格布局简称为 TH
    • 相对布局简称为 R
    • 浮动布局简称 FO
    • 流式布局 FL
    • 路径布局简称 P
    • 布局视图的非布局父视图 S
    • 所有布局简称 ALL

    ####布局尺寸类 TGLayoutSize 布局尺寸类用来描述视图布局核心中的视图尺寸。我们对 UIView 扩展出了 2 个布局尺寸对象 :

        public var tg_width:TGLayoutSize
        public var tg_height:TGLayoutSize
    

    分别用来实现视图的宽度和高度的布局尺寸设置。在 TGLayoutSize 类中,我们可以通过方法equal来设置视图尺寸的多种类型的值,类中是通过重载 equal 方法来实现多种类型的值的设置的。

    
        public func equal(_ size:CGFloat, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
        public func equal(_ weight:TGWeight, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
        public func equal(_ array:[TGLayoutSize], increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
        public func equal(_ view:UIView,increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
        public func equal(_ dime:TGLayoutSize!, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    

    上面的方法中我们可以通过 equal 方法来设置:

    • CGFloat 类型的值表示视图的尺寸是一个绝对值类型的尺寸值。比如:
    A.tg_width.equal(100)  //A 的宽度为 100
    A.tg_height.equal(200) //A 的高度为 200
    
    • TGWeight 类型的值表示视图的尺寸是一个依赖于父视图尺寸的相对比例值。(具体见上面 TGWeight 类型值的定义和使用)
    //假如 A 是在一个垂直线性布局下的子视图
    A.tg_width.equal(20%)   //A 的宽度是父布局视图宽度的 20%
    A.tg_height.equal(30%)  //A 的高度是父布局视图剩余高度的 30%
    
    • TGLayoutSize 类型的值表示视图的尺寸和另外一个尺寸对象的值相等,这也是一种相对值类型的尺寸值,通过设置这种尺寸的依赖我们就可以不必要明确的指定一个具体的值,而是会随着所以依赖的尺寸变化而变化。设置为 TGLayoutSize 类型的值通常用于在相对布局中的子视图,当然也可以在其他类型的布局中使用。下面是一个展示的例子:
      A.tg_width.equal(B.tg_width)  //A 的宽度等于 B 的宽度
      A.tg_height.equal(A.tg_width)  //A 的高度等于 A 的宽度
    
    6 条回复    2016-11-30 15:22:32 +08:00
    loveuqian
        1
    loveuqian  
       2016-11-29 10:25:59 +08:00
    先给排版点个赞

    然后有个问题
    为什么现在会有这么多的布局框架
    老牌的两个, masonry 和 snapkit 我相信是最多人用的
    而且大多数人我想都习惯了

    能否把你的框架对比一下
    fish420
        2
    fish420  
       2016-11-29 17:22:13 +08:00 via iPhone
    感谢分享
    tuimaochang
        3
    tuimaochang  
       2016-11-29 18:46:16 +08:00
    谢谢楼主分享好人一生平安
    Hysteria
        4
    Hysteria  
       2016-11-30 13:12:30 +08:00
    我是这么觉得的,这种布局框架就选使用人数最多的一种用就好了。
    NikoTiz
        5
    NikoTiz  
       2016-11-30 13:40:24 +08:00
    我是觉得字少一点比较好
    Zero24
        6
    Zero24  
       2016-11-30 15:22:32 +08:00
    好长
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2821 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 06:21 · PVG 14:21 · LAX 22:21 · JFK 01:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.