Flexbox和Texture

本文是<> 第二十七篇学习笔记.

Flexbox 是 React Native、Weex 和 Texture(AsyncDisplayKit)这些知名布局库采用的布局思路。

苹果公司官方的 UIStackView,也是采用 Flexbox 思路来实现布局的。

Flexbox 好在哪

iOS 系统提供的布局方式有两种:

  • 一种是 Frame 这种原始方式,也就是通过设置横纵坐标和宽高来确定布局。

  • 另一种是自动布局(Auto Layout),相比较于 Frame 需要指出每个视图的精确位置,自动布局对于视图位置的描述更加简洁和易读,只需要确定两个视图之间的关系就能够确定布局。

那么在这种情况下,我们为什么还要关注其他布局思路呢?

  • 自动布局思路本身还可以再提高。Flexbox 比自动布局提供了更多、更规范的布局方法,布局方式考虑得更全面,使用起来也更加方便。同时,苹果公司基于 Flexbox 的布局思路,又在自动布局之上封装了一层 UIStackView。

  • 针对多个平台的库需要使用更加通用的布局思想。Flexbox 在 2009 年被 W3C 提出,可以很简单、完整地实现各种页面布局,而且还是响应式的,应用于前端领域,目前所有浏览器都已支持。后来通过 React Native 和 Weex 等框架,它被带入到客户端开发中,同时支持了 iOS 和 Android。

与自动布局思路类似,Flexbox 使用的也是描述性的语言来布局。使用 Flexbox 布局的视图元素叫 Flex 容器(flex container),其子视图元素叫作 Flex 项目(flex item)。

image.png

Flexbox 算法

Flexbox 算法的主要思想是,让 flex 容器能够改变其 flex 项目的宽高和顺序,以填充可用空间,flex 容器可以通过扩大项目来填充可用空间,或者缩小项目以防止其超出其可用空间。


首先,创建一组匿名的 flex 项目,按照这组匿名 flex 项目设置的排列规则对其进行排列。

  • 第一步,确定 flex 项目的 main space 和 cross space,如果 flex 容器定义了大小就直接使用定义的大小;否则, 从 flex 容器的可用空间里减去 margin、border、padding。

  • 第二步,确定每个项目 的 flex base 大小和假设的大小,其中假设的大小是项目依据它最小和最大的大小属性来确定的。flex 容器的大小,由它的大小属性来确定。

这个计算过程中,flex 容器的最小内容大小,是由它所有项目的最小内容大小之和算出的;而 flex 容器的最大内容大小,则是由它所有项目的最大内容大小之和确定出。


接着,将 flex 项目收集到 flex lines 中。如果 flex 容器是单行,那么就把所有的 flex 项目都收集到单个 flex line 里。

否则,就从第一个未收集的项目开始尽可能多地收集 flex 项目到 flex line 里,根据 flex 容器的 inner 大小判断是否当前 flex line 收集满。重复操作,直到将所有 flex 项目都被收集到了 flex lines 里。

处理完 flex lines 后,需要通过使用过的大小和可用大小来确定每个项目的 cross 大小,然后计算每个 flex line 的 cross 大小以及 flex line 里每个 flex 项目的 cross 大小。


最后,进行 Main-Axis 对齐和 Cross-Axis 对齐。

  • Main-Axis 对齐就是分配剩余空间。对于每个 flex line,如果有剩余空间, margin 设置为 auto 的话,就平均分配剩余空间。

  • Cross-Axis 对齐,先要解决自动 margin,然后沿 cross-axis 对齐所有 flex items;随后确定 flex container 使用的 cross 大小;最后对齐所有 flex lines。

image.png

如图中所示,其中 View 类似 flex container,View 的 Subviews 类似 flex items,flexbox 的算法简而言之就是:首先依据 View 的 margin、padding、border 确定出横纵大小,接下来确定排列,根据 View 的大小确定 Subviews 的行内容,确定出行中每个 Subview 的大小,最终确定出 Subview 的位置。

Texture

Texture(AsyncDisplayKit)是一款基于UIKit构建的iOS框架,其目的在于使复杂的界面依然能够保持流畅和响应。

官方文档: https://texturegroup.org/docs/installation.html
github地址: https://github.com/TextureGroup/Texture

Texture将图像解码、文本绘制和渲染等操作从主线程中迁移到子线程,从而保证主线程的响应。

Texture 框架的提供了基于 Flexbox的布局 ,性能远好于苹果的自动布局,而且写起来更简单。

Texture的安装

直接使用CocoaPods进行安装

pod 'Texture'

Texture的核心概念

Node

Node是Texture的基本单位,ASDisplayNode是对UIVew的抽象对象,而UIView是对CALayer的抽象。和只能在主线程使用的view不同,node是线程安全的,即可以在后台线程上实例化和配置它们的整个层次结构。

对于熟练使用UIView和CALayer的人来说,使用Node将会是一件驾轻就熟的事。因为大多数的方法在Node中都能找到对应,并且UIView和CALayer的大多数属性在Node中亦存在可用,且命名相同。

Texture提供了各种Node去替换我们习惯使用的UIKit中的控件,帮助我们完成对大型应用的编写。

Node容器

Node容器可以视作是UIKit和Texture之间的集成点。这些容器负责告知所包含的Node它们当前处于什么状态,以便可以加载数据并尽可能高效地呈现Node。

在开始使用Texture时一个常见的错误是我们将一个node直接添加于一个view结构中,这样做将会导致他们在渲染时发生闪烁。所以我们正确的用法是将Node作为节点容器的子节点进行添加使用。以下是常用的节点容器类:

Node Containers UIKit Equivalent
ASCollectionNode 相当于UICollectionView
ASPagerNode 相当于UIPageViewController
ASTableNode 相当于UITableView
ASViewController 相当于UIViewController
ASNavigationController 相当于UINavigationController,实现ASVisibility协议
ASTabBarController 相当于UITabBarController,实现ASVisibility协议

节点容器自动管理其节点的智能预加载。也就是说所有节点的布局测量、数据获取、解码和渲染都将异步完成。

Node子类

Texture提供了如下Node子类:

Node Subclass UIKit Equivalent
ASDisplayNode Texture 的根节点,所有其他的节点继承于此,相当于UIView
ASCellNode 相当于 UITableViewCell 和 UICollectionViewCell ,可用于容器 ASTableNode、ASCollectionNode和 ASPagerNode
ASSCrollNode 相当于 UIScrollView,此节点用于创建自定义的包含其他节点的滚动区域。
ASEditableTextNode 相当于UITextView
ASTextNode 相当于UILabel
ASImageNode 相当于UIImage
ASNetworkImageNode 相当于UIImage
ASMultiplexImageNode 相当于UIImage
ASVideoNode 相当于AVPlayerLayer
ASVideoPlayerNode 相当于UIMoviePlayer
ASControlNode 相当于UIControl
ASButtonNode 相当于UIButton
ASMapNode 相当于MKMapView

虽然这些节点子类与UIKit组件极大相似,但纹理节点提供更高级的特性和便利性。

image.png
智能预载

Texture除了实现节点的异步和同时渲染和测量,还有智能预载。在前文提到Texture渲染和测量从主线程中迁移,这是因为所有节点都具当前界面状态的概念。这个界面状态通过ASRangeController进行更新,所有容器都在其内部进行创建和维护。

通常情况下,当节点被添加到一个可滚动或拖动的界面,在界面滚动时,节点的界面状态将在它们移动时更新。一个节点将会处在以下几个界面范围中:

  • Preload:距离可见最远的范围,从外部获取内容资源的地方

  • Display:在这里将会执行文本栅格化和图片解码的任务

  • Visible:在屏幕上可见

image.png

在快速了解到Texture的核心概念和节点后,我们就可以开始使用Texture来实现负责且流畅的界面了。

节点实例/Subclassing

创建子类时最重要的区别是你使用的是 ASViewController 还是 ASDisplayNode,这听起来很明显,但是因为这其中有一些微妙的差异,所以记住这点还是相当重要的。

ASDisplayNode

虽然实例化节点与 UIView 类似,但需要遵循一些原则,以确保你充分利用了它的能力,并确保节点按照预期的方式运行。

-init
在使用 initNodeBlocks 时,这个方法会后台线程上被调用。但是,因为没有其他方法会在 init 完成之前运行,所以这个方法不需要加锁。

需要记住的最重要的一点是,init 方法必须能够在任何队列上调用。最值得注意的是,你永远不应该在节点初始化方法中初始化任何 UIKit 对象,以及调用 node.layer node.view.x 等与viewlayer 有关的操作,也不应该在这个方法中为节点添加手势,这些事件应该在 didLoad 方法中进行。

-didLoad

这个方法在概念上类似于 UIViewController-viewDidLoad 方法,当后台视图初始化完成时,它会被调用一次,它保证会在主线程上被调用,是执行任何 UIKit 代码合适的地方,例如添加手势识,更改 viewlayer,初始化 UIKit 对象。

-layoutSpecThatFits:

该方法定义了节点的布局,并在后台线程上进行了大量的计算。此方法是你声明、创建和修改 ASLayoutSpec 布局描述对象的地方,该对象描述了节点的 size,以及其子节点的 size 和 position,是你放置大部分布局代码的地方。

ASLayoutSpec 对象直到在此方法中返回前是可变的。 在这之后,这个对象将不可改变,需要注意的是你不需要缓存 ASLayoutSpec 对象以备后用,我们建议你在必要时重新创建布局描述。

由于它在后台线程上运行,因此你不能在这个方法中调用 node.viewnode.layer 以及它们的属性。 此外,除非你明确知道自己在做什么,否则不要在此方法中创建其他节点。 另外,重写此方法并不一定需要调用 super 方法。

-layout

在此方法中调用 super 将,会使用 layoutSpec 对象计算布局,所有子节点都将计算其 size 和 position。

-layout 在概念上类似于 UIViewController-viewwilllayoutsubview,这是一个更改 hidden 属性、修改 view 属性、设置背景颜色的好地方。你可以在 -layoutspec: 方法中设定背景颜色,但这可能会存在时序问题。如果你需要使用原生的 UIView,可以在这里设置它们的 frame,不管怎样,你始终可以使用 -initWithViewBlock: 创建节点,并在其他地方的后台线程中进行调整。

这个方法在主线程上被调用,如果你使用的是 ASLayoutSpec,那么你不应该过多地依赖这个方法,因为在主线程上进行布局是非常可取的,需要这个方法的子类小于 1/10。

使用 -layout 的一个重要用途是你需要子节点的 size 是精确的。举例来说,当你希望一个 collectionNode 可以铺面屏幕,这种情况不被 ASLayoutSpec 很好的支持,此时最简单的做法是在这个方法中手动设定 frame

subnode.frame = self.bounds

如果你希望在 ASViewController 中得到相同的效果,那么你可以在 -viewWillLayoutSubviews 中做同样的事情,不过如果你的节点通过 initWithNode: 进行实例化,它会自动做到这一点。

ASViewController

ASViewController 是一个常规的 UIViewController 子类,它具有管理节点的特殊功能。因为它是一个 UIViewController 子类,所以所有的方法都在主线程上被调用,并且你应该在主线程上 ASViewController

-init

这个方法在 ASViewController 的生命周期开始时被调用一次,与 UIViewController 的初始化一样,你最好不要在这个方法中访问 self.viewself.node.view,因为这样会强制创建 view。 这些操作可以在 -viewDidLoad 中进行,-viewDidLoad 可以执行任何 view 的访问。

ASViewController 指定的构造器是 initWithNode:,一个典型的构造器看起来就像下面的代码。请注意下面的代码,在调用 super 之前,ASViewController 的节点是如何被创建的,ASViewController 管理节点类似于 UIViewController 管理视图,但是它的初始化方式有所区别:

class TZYVC: ASViewController {

    init() {
        let pagerNode = ASPagerNode()
        super.init(node: pagerNode)
        pagerNode.setDataSource(self)
        pagerNode.setDelegate(self)
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
extension TZYVC: ASPagerDataSource {
    func numberOfPages(in pagerNode: ASPagerNode) -> Int {
        return 10
    }
}
extension TZYVC: ASPagerDelegate {
}

-loadView

我们建议你不要使用这个方法,因为与 -viewDidLoad 相比,它没有什么特别的优势,并且有一些缺点。 但是,只要不将 self.view 属性设置为不同的值,它可以安全的使用。 它的 super 方法会将其封装的 UIViewControllerview 设置为 ASViewControllernode.view

-viewDidLoad

这个方法在 -loadView 之后被执行,这是 ASViewController 生命周期中,你可以访问 node.view 最早的方法,你可以在这份方法中任意修改 viewlayer 或添加手势,这个方法在其所属的生命周期中,只会执行一次。

所以布局代码不应该放在这个方法中,因为当界面重绘时,这里的代码不会被再次调用。UIViewController 中这个方法也是同样的,在这种方法中放置布局代码是一种不太好的做法,即使你的布局不会因为交互发生变化。

-viewWillLayoutSubviews

这个方法会与节点的 -layout 同时调用,它可能在 ASViewController 的生命周期中被多次调用,当 ASViewController 的节点的边界发生改变,如旋转、分割屏幕、键盘弹出等行为,或者当视图的层次结构发生变化,如子节点添加、删除或改变大小时,这个方法将被调用。

因为它不经常被调用,但是调用就代表页面需要重绘,因此所有的布局代码最好都放在这个方法中,即使是不直接依赖于 size 的 UI 代码也应放在这里。

-viewWillAppear: / -viewDidDisappear:

viewWillAppearASViewController 的节点出现在屏幕上之前被调用,这是节点从屏幕出现的最早时间,viewDidDisappear 在控制器从视图层次结构中移除之后被调用,这是节点从屏幕消失的最早时机,这两个方法提供了一个很好的时机来启动或停止与控制器相关的动画,这也是一个保存和记录用户行为日志的好地方。

尽管这些方法可能被多次调用,并且是可以执行布局代码的,但是这两个方法不会在所有需要重绘的时候被调用,因此除了特定的动画设置之外,不应该用于执行核心的布局代码。

一些 Node 需要设定 size

一些Node根据元素的即时可用内容,有一个固有大小,比如,ASTextNode 可以根据 .string 属性,确定自身的 size,其他具有固有大小的 Node 有:

  • ASImageNode
  • ASTextNode
  • ASButtonNode

其他的 Node 在加载外部资源之前,或没有固有大小,或缺少一个固有大小。例如,在从 URL 下载图像之前,ASNetworkImageNode 并不能确定它的大小,这些元素包括:

  • ASVideoNode
  • ASVideoPlayerNode
  • ASNetworkImageNode
  • ASEditableTextNode

缺少初始固有大小的这些 Node 必须使用 ASRatioLayoutSpec(比例布局规则)ASAbsoluteLayoutSpec(绝对布局规则) 或样式对象的 .size 属性为它们设置初始大小。

Texture布局

Texture 考虑到布局扩展性,提供了一个基类 ASLayoutSpec。这个基类 提供了布局的基本能力,使 Texture 可以通过它扩展实现多种布局思路,比如 Wrapper、Inset、Overlay、Ratio、Relative、Absolute 等布局思路,也可以继承 ASLayoutSpec 来自定义你的布局算法。

Texture的Layout API相较于复杂且增加开销的Auto Layout其具有以下的有点:

  • 快捷:比Auto Layout 布局快速
  • 异步并发:可以在后台线程中对布局进行计算从而不影响交互
  • 规范:使用不可变数据结构声明布局
  • 可缓存
  • 可扩展

Texture布局主要围绕这两个中心概念:

  • Layout Elements
  • Layout Specs
Layout Specs

ASLayoutSpec 是所有布局规则的父类,它的主要工作是处理和管理所有的子类,它也可以用来创建自定义的布局规则。不过创建 ASLayoutSpec 的自定义子类是一项 super advanced 级别的操作,如果你有这方面的需要,建议你尝试将我们提供的布局规则进行组合,以创建更高级的布局。

ASLayoutSpec 的另一个用途是应用了 .flexShrink 或者 .flexGrow 是,在 ASStackLayoutSpec 中作为一个 spacer 和其他子节点一起使用,

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
  ...
  let spacer = ASLayoutSpec()
  spacer.style.flexGrow = 1.0

  stack.children = [imageNode, spacer, textNode]
  ...
}

简单介绍
结合布局规则和布局元素制作复杂界面

在这里,你可以看到黄色突出显示的 ASTextNodes,顶部图像 ASVideoNode 和盒子布局规则 ASStackLayoutSpec 是如何组合并创建了一个复杂界面。

image.png

使用中心布局规则 ASCenterLayoutSpec 和覆盖布局规则 ASOverlayLayoutSpec,来放置顶部图像 ASVideoNode 中的播放按钮。

image.png

布局规范可以作为一个盛放布局元素的容器通过理解布局元素的布局来使他们可以相互关联。
Texture提供了几个ASLayoutSpec的子类:

  • ASAbsoluteLayoutSpec // 绝对布局

字面上的意思,绝对布局,在iOS开发上都熟。

ASAbsoluteLayoutSpec 中你可以通过设置它们的 layoutPosition 属性来指定其子节点的横纵坐标。 绝对布局比其他类型的布局相比,不太灵活且难以维护。

ASAbsoluteLayoutSpec 有一个属性:

sizing:

确定 ASAbsoluteLayoutSpec 将占用多少空间,可选值包括:Default,Size to Fit。请注意,Size to Fit 将复制旧的 ASStaticLayoutSpec 的行为。

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
  let maxConstrainedSize = constrainedSize.max

  // 在一个静态布局中,使用 ASAbsoluteLayoutSpec 布局所有子节点
  guitarVideoNode.style.layoutPosition = CGPoint.zero
  guitarVideoNode.style.preferredSize = CGSize(width: maxConstrainedSize.width, height: maxConstrainedSize.height / 3.0)

  nicCageVideoNode.style.layoutPosition = CGPoint(x: maxConstrainedSize.width / 2.0, y: maxConstrainedSize.height / 3.0)
  nicCageVideoNode.style.preferredSize = CGSize(width: maxConstrainedSize.width / 2.0, height: maxConstrainedSize.height / 3.0)

  simonVideoNode.style.layoutPosition = CGPoint(x: 0.0, y: maxConstrainedSize.height - (maxConstrainedSize.height / 3.0))
  simonVideoNode.style.preferredSize = CGSize(width: maxConstrainedSize.width / 2.0, height: maxConstrainedSize.height / 3.0)

  hlsVideoNode.style.layoutPosition = CGPoint(x: 0.0, y: maxConstrainedSize.height / 3.0)
  hlsVideoNode.style.preferredSize = CGSize(width: maxConstrainedSize.width / 2.0, height: maxConstrainedSize.height / 3.0)

  return ASAbsoluteLayoutSpec(children: [guitarVideoNode, nicCageVideoNode, simonVideoNode, hlsVideoNode])
}
  • ASWrapperLayoutSpec // 填充布局

作为ASLayoutSpec一个简单的子类,它可以包裹ASLayoutElement并根据布局元素上设置的大小计算子项的布局。

ASWrapperLayoutSpec 是一个简单的 ASLayoutSpec 子类,它可以封装了一个 LayoutElement,并根据 LayoutElement 上设置的大小计算其布局及子元素布局。

ASWrapperLayoutSpec 可以轻松的从 -layoutSpecThatFits: 中返回一个 subnode。 你可以在这个 subnode 上设定 size ,但是如果你需要设定 .position ,请使用 ASAbsoluteLayoutSpec

// 返回一个 subnode
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec 
{
  return ASWrapperLayoutSpec(layoutElement: _subnode)
}

// 设定 size,但不包括 position。
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec 
{
  _subnode.style.preferredSize = CGSize(width: constrainedSize.max.width,
                                        height: constrainedSize.max.height / 2.0)
  return ASWrapperLayoutSpec(layoutElement: _subnode)
}
  • ASStackLayoutSpec // 盒子布局

在 Texture 中的所有 layoutSpec 中,ASStackLayoutSpec 是最有用的,也是最强大的。 ASStackLayoutSpec 使用 flexbox 来确定其子元素的 sizeposition 。 Flexbox 旨在为不同的屏幕尺寸提供一致的布局, 在盒子布局中,你垂直或水平的对其元素。 盒子布局也可以是另一个盒子的子布局,这使得盒子布局规则几乎可以胜任任何的布局。

除了 ASLayoutElement 属性,ASStackLayoutSpec 还有 7 个属性:
direction
指定子元素的排序方向,如果设置了 horizontalAlignmentverticalAlignment,它们将被再次解析,这会导致 justifyContentalignItems 也会相应地更新。

spacing
描述子元素之间的距离

horizontalAlignment
指定子元素如何在水平方向上对齐,它的实际效果取决于 direction,设置对齐会使 justifyContentalignItems 更新。在 direction 改变之后,对齐方式仍然有效,因此,这是一个优先级高的属性。

verticalAlignment
指定子元素如何在垂直方向上对齐,它的实际效果取决于 direction,设置对齐会使 justifyContentalignItems 更新。在 direction 改变之后,对齐方式仍然有效,因此,这是一个优先级高的属性。

justifyContent
指定子元素沿着主轴的排列方式

alignItems
指定子元素沿着次轴的排列方式

flexWrap
子元素是否堆叠为多行

alignContent
当子元素堆叠为多行时,多行的对齐方式

  • ASInsetLayoutSpec // 边距布局

在布局过程中,ASInsetLayoutSpec 将其 constrainedSize.max 减去其 insets 的 CGSize 传递给它的子节点, 一旦子节点确定了它的 sizeinsetSpec 将它的最终 size 作为子节点的 sizemargin

image.png
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec 
{
  let mainStack = ASStackLayoutSpec(direction: .horizontal,
                                    spacing: 6.0,
                                    justifyContent: .start,
                                    alignItems: .center,
                                    children: [titleNode, subtitleNode])

  // 设置盒子约束大小
  mainStack.style.minWidth = ASDimensionMakeWithPoints(60.0)
  mainStack.style.maxHeight = ASDimensionMakeWithPoints(40.0)

  return mainStack
}

由于 ASInsetLayoutSpec 是根据其子节点的 size 来确定的,因此子节点必须具有固有大小或明确设置其 size

我们可以使用INFINITY作为插入间距中的值来保持子元素原有的尺寸

  • ASOverlayLayoutSpec // 覆盖布局

ASOverlayLayoutSpec 将其上面的子节点(红色)延伸,覆盖一个子节点(蓝色)。

overlaySpecsize 根据子节点的 size 计算, 在下图中,子节点是蓝色的层,然后将子节点的 size 作为 constrainedSize 传递给叠加布局元素(红色), 因此,重要的一点是,子节点(蓝色)必须具有固有大小或明确设置 size

image.png
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
  let backgroundNode = ASDisplayNodeWithBackgroundColor(UIColor.blue)
  let foregroundNode = ASDisplayNodeWithBackgroundColor(UIColor.red)
  return ASOverlayLayoutSpec(child: backgroundNode, overlay: foregroundNode)
}
  • ASBackgroundLayoutSpec // 背景布局

ASBackgroundLayoutSpec 设置一个子节点(蓝色)为内容,将背后的另一个子节点拉伸为背景(红色)。

ASBackgroundLayoutSpecsize 根据子节点的 size 确定,在下图中,子节点是蓝色层,子节点的 size 作为 constrainedSize 传递给背景图层(红色),因此重要的一点是,子节点(蓝色)必须有一个固有大小或明确设置 size

image.png

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
  let backgroundNode = ASDisplayNodeWithBackgroundColor(UIColor.red)
  let foregroundNode = ASDisplayNodeWithBackgroundColor(UIColor.blue)
  return ASBackgroundLayoutSpec(child: foregroundNode, background: backgroundNode)
}
  • ASCenterLayoutSpec // 居中布局

ASCenterLayoutSpec 将其子节点的中心设置为最大的 constrainedSize 的中心。

如果 ASCenterLayoutSpec 的宽度或高度没有设定约束,那么它会缩放到和子节点的宽度或高度一致。

ASCenterLayoutSpec 有两个属性:

centeringOptions:

决定子节点如何在 ASCenterLayoutSpec 中居中,可选值包括:None,X,Y,XY。

sizingOptions:

决定 ASCenterLayoutSpec 占用多少空间,可选值包括:Default,minimum X,minimum Y,minimum XY。

image.png
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
  let subnode = ASDisplayNodeWithBackgroundColor(UIColor.green, CGSize(width: 60.0, height: 100.0))
  let centerSpec = ASCenterLayoutSpec(centeringOptions: .XY, sizingOptions: [], child: subnode)
  return centerSpec
}
  • ASRatioLayoutSpec // 比例布局

ASRatioLayoutSpec 可以以固定的宽高比来缩放子节点。 这个规则必须将一个宽度或高度传递给它作为一个 constrainedSize,因为它使用这个值来进行计算。

使用 ASRatioLayoutSpecASNetworkImageNodeASVideoNode 提供固有大小是非常常见的,因为两者在内容从服务器返回之前都没有固有大小。

image.png
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
  // 将 subnode 缩放一半
  let subnode = ASDisplayNodeWithBackgroundColor(UIColor.green, CGSize(width: 100, height: 100.0))
  let ratioSpec = ASRatioLayoutSpec(ratio: 0.5, child: subnode)
  return ratioSpec
}
  • ASRelativeLayoutSpec // 顶点布局

根据垂直和水平位置说明符布置组件并将其放置在布局边界内。

根据水平位置和垂直位置的设定,将一个子节点放置在九宫格布局规则中的任意位置。

这是一个非常强大的布局规则,但是它非常复杂,在这个概述中无法逐一阐述, 有关更多信息,请参阅 ASRelativeLayoutSpec-calculateLayoutThatFits: 方法和属性。

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
  ...
  let backgroundNode = ASDisplayNodeWithBackgroundColor(UIColor.blue)
  let foregroundNode = ASDisplayNodeWithBackgroundColor(UIColor.red, CGSize(width: 70.0, height: 100.0))

  let relativeSpec = ASRelativeLayoutSpec(horizontalPosition: .start,
                                          verticalPosition: .start,
                                          sizingOption: [],
                                          child: foregroundNode)

  let backgroundSpec = ASBackgroundLayoutSpec(child: relativeSpec, background: backgroundNode)
  ...
}
  • ASCornerLayoutSpec // 角标布局
    ASCornerLayoutSpec是一种用于快速角元素布局的规范。通过使用声明性代码表达式而不是手动坐标计算的方法在角落中定位元素。

ASLayoutSpec 子类实现了各种布局思路,ASLayoutSpec 制定各种布局相通的协议方法,遵循这些协议后可以保证这些子类能够使用相同的规则去实现更丰富的布局。

通过 ASLayoutSpec 遵循的 ASLayoutElement 协议,可以知道 ASLayoutSpec 提供的基本能力有哪些。ASLayoutElement 协议定义如下:

@protocol ASLayoutElement 
#pragma mark - Getter
@property (nonatomic, readonly) ASLayoutElementType layoutElementType;
@property (nonatomic, readonly) ASLayoutElementStyle *style;
- (nullable NSArray> *)sublayoutElements;

#pragma mark - Calculate layout

// 要求节点根据给定的大小范围返回布局
- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize;
// 在子 layoutElements 上调用它来计算它们在 calculateLayoutThatFits: 方法里实现的布局
- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize;
// 重写此方法以计算 layoutElement 的布局
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize;
// 重写此方法允许你接收 layoutElement 的大小。使用这些值可以计算最终的约束大小。但这个方法要尽量少用
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
                     restrictedToSize:(ASLayoutElementSize)size
                 relativeToParentSize:(CGSize)parentSize;

- (BOOL)implementsLayoutMethod;

@end

协议定义了layoutThatFitscalculateLayoutThatFits 等回调方法。其中,layoutThatFits 回调方法用来要求节点根据给定的大小范围返回布局,重写 calculateLayoutThatFits 方法用以计算 layoutElement 的布局。定义了统一的协议方法,能让 ASLayoutSpec 统一透出布局计算能力,统一规范的协议方法,也有利于布局算法的扩展。

ASLayoutSpec 的子类中,应用最广泛的 ASStackLayoutSpec和 iOS 中自带的 UIStackView 类似,布局思路参照了 Flexbox,比如 horizontalAlignment、alignItems、flexWrap 等属性很容易和 Flexbox 对应上。

布局调试/Layout Debugging

在任何 ASDisplayNodeASLayoutSpec 上调用 -asciiArtString 都会返回该对象及其子项的字符图。 你也可以在任何 NodelayoutSpec 中设置 .debugName,这样也将包含字符图,下面是一个示例:

-----------------------ASStackLayoutSpec----------------------
|  -----ASStackLayoutSpec-----  -----ASStackLayoutSpec-----  |
|  |       ASImageNode       |  |       ASImageNode       |  |
|  |       ASImageNode       |  |       ASImageNode       |  |
|  ---------------------------  ---------------------------  |
--------------------------------------------------------------

你还可以在任何 ASLayoutElement ,比如 NodelayoutSpec 上打印样式对象,这在调试 .size 属性时特别有用。

(lldb) po _photoImageNode.style
Layout Size = min {414pt, 414pt} <= preferred {20%, 50%} <= max {414pt, 414pt}
Layout Elements

布局规范(ASLayoutSpec)由布局元素构成并对他们进行排列,并且所有的节点(ASDisplayNode)和布局规范都遵循了ASLayoutElement的协议,这意味着我们可以通过节点和其他作为子元素的规范来组合实现复杂的布局规范。

  • ASStackLayoutElement Properties:只会在盒子布局中的的 subnodelayoutSpec 中生效;

.style.spacingBefore

CGFloat 类型,direction 上与前一个 node 的间隔。

.style.spacingAfter

CGFloat 类型,direction 上与后一个 node 的间隔。

.style.flexGrow

Bool 类型,子节点尺寸总和小于 minimum ,即存在剩余空间时,是否放大。

.style.flexShrink

Bool 类型,子节点总和大于 maximum,即空间不足时,是否缩小。

.style.flexBasis

ASDimension 类型,描述在剩余空间是均分的情况下,应用 flexGrowflexShrink 属性之前,该对象在盒子中垂直或水平方向的初始 size

.style.alignSelf

ASStackLayoutAlignSelf 类型,描述对象在十字轴的方向,此属性会覆盖 alignItems,可选值有:

  • ASStackLayoutAlignSelfAuto
  • ASStackLayoutAlignSelfStart
  • ASStackLayoutAlignSelfEnd
  • ASStackLayoutAlignSelfCenter
  • ASStackLayoutAlignSelfStretch

.style.ascender

CGFloat 类型,用于基线对齐,描述对象从顶部到其基线的距离。

.style.descender

CGFloat 类型,用于基线对齐,描述对象从基线到其底部的距离。

  • ASAbsoluteLayoutElement Properties:只会在绝对布局中的的 subnodelayoutSpec 中生效;

.style.layoutPosition

CGPoint 类型,描述该对象在 ASAbsoluteLayoutSpec 父规则中的位置。

  • ASLayoutElement Properties:适用于所有 NodelayoutSpec

.style.width
ASDimension 类型,width 属性描述了 ASLayoutElement 内容区域的宽度。 minWidth 和 maxWidth 属性会覆盖 width, 默认值为 ASDimensionAuto。

.style.height
ASDimension 类型,height 属性描述了 ASLayoutElement 内容区域的高度。 minHeight 和 maxHeight 属性会覆盖 height,默认值为 ASDimensionAuto。

.style.minWidth
ASDimension 类型,minWidth 属性用于设置一个特定布局元素的最小宽度。 它可以防止 width 属性的使用值小于 minWidth 指定的值,minWidth 的值会覆盖 maxWidth 和 width。 默认值为 ASDimensionAuto。

.style.maxWidth
ASDimension 类型,maxWidth 属性用于设置一个特定布局元素的最大宽度。 它可以防止 width 属性的使用值大于 maxWidth 指定的值,maxWidth 的值会覆盖 width,minWidth 会覆盖 maxWidth。 默认值为 ASDimensionAuto。

.style.minHeight
ASDimension 类型,minHeight 属性用于设置一个特定布局元素的最小高度。 它可以防止 height 属性的使用值小于 minHeight 指定的值。 minHeight 的值会覆盖 maxHeight 和 height。 默认值为 ASDimensionAuto。

.style.maxHeight
ASDimension 类型,maxHeight 属性用于设置一个特定布局元素的最大高度,它可以防止 height 属性的使用值大于 maxHeight 指定的值。 maxHeight 的值会覆盖 height,minHeight 会覆盖 maxHeight。 默认值为 ASDimensionAuto。

.style.preferredSize
CGSize 类型, 建议布局元素的 size 应该是多少。 如果提供了 minSize 或 maxSize ,并且 preferredSize 超过了这些值,则强制使用 minSize 或 maxSize。 如果未提供 preferredSize,则布局元素的 size 默认为 calculateSizeThatFits: 方法提供的固有大小。

此方法是可选的,但是对于没有固有大小或需要用与固有大小不同的的 size 进行布局的节点,则必须指定 preferredSize 或 preferredLayoutSize 中的一个,比如没这个属性可以在 ASImageNode 上设置,使这个节点的 size 和图片 size 不同。

警告:当 size 的宽度或高度是相对值时调用 getter 将进行断言。

.style.minSize
CGSize 类型,可选属性,为布局元素提供最小尺寸,如果提供,minSize 将会强制使用。 如果父级布局元素的 minSize 小于其子级的 minSize,则强制使用子级的 minSize,并且其大小将扩展到布局规则之外。

例如,如果给全屏容器中的某个元素设置 50% 的 preferredSize 相对宽度,和 200pt 的 minSize 宽度,preferredSize 会在 iPhone 屏幕上产生 160pt 的宽度,但由于 160pt 低于 200pt 的 minSize 宽度,因此最终该元素的宽度会是 200pt。

.style.maxSize
CGSize 类型,可选属性,为布局元素提供最大尺寸,如果提供,maxSize 将会强制使用。 如果子布局元素的 maxSize 小于其父级的 maxSize,则强制使用子级的 maxSize,并且其大小将扩展到布局规则之外。

例如,如果给全屏容器中的某个元素设置 50% 的 preferredSize 相对宽度,和 120pt 的 maxSize 宽度,preferredSize 会在 iPhone 屏幕上产生 160pt 的宽度,但由于 160pt 高于 120pt 的 maxSize 宽度,因此最终该元素的宽度会是 120pt。

.style.preferredLayoutSize
ASLayoutSize 类型,为布局元素提供建议的相对 size。 ASLayoutSize 使用百分比而不是点来指定布局。 例如,子布局元素的宽度应该是父宽度的 50%。 如果提供了可选的 minLayoutSize 或 maxLayoutSize,并且 preferredLayoutSize 超过了这些值,则将使用 minLayoutSize 或 maxLayoutSize。 如果未提供此可选值,则布局元素的 size 将默认是 calculateSizeThatFits: 提供的固有大小。

.style.minLayoutSize
ASLayoutSize 类型, 可选属性,为布局元素提供最小的相对尺寸, 如果提供,minLayoutSize 将会强制使用。 如果父级布局元素的 minLayoutSize 小于其子级的 minLayoutSize,则会强制使用子级的 minLayoutSize,并且其大小将扩展到布局规则之外。

.style.maxLayoutSize
ASLayoutSize 类型, 可选属性,为布局元素提供最大的相对尺寸。 如果提供,maxLayoutSize 将会强制使用。 如果父级布局元素的 maxLayoutSize 小于其子级的 maxLayoutSize,那么将强制使用子级的 maxLayoutSize,并且其大小将扩展到布局规则之外。

Layout API Sizing

理解 Layout API 的各种类型最简单方法是查看所有单位之间的相互关系。

image.png

ASDimension

ASDimension 是一个正常的 CGFloat,支持表示一个 pt 值,一个相对百分比值或一个自动值,这个单位允许一个的 API 同时使用固定值和相对值。

// 返回一个相对值
ASDimensionMake("50%")
ASDimensionMakeWithFraction(0.5)

// 返回一个 pt 值
ASDimensionMake("70pt")
ASDimensionMake(70)
ASDimensionMakeWithPoints(70)

使用 ASDimension 的示例:

ASDimension用于设置 ASStackLayoutSpec 子元素的 flexBasis 属性。 flexBasis 属性根据在盒子排序方向是水平还是垂直,来指定对象的初始大小。在下面的视图中,我们希望左边的盒子占据水平宽度的 40%,右边的盒子占据宽度的 60%,这个效果我们可以通过在水平盒子容器的两个 childen 上设置 .flexBasis 属性来实现:

self.leftStack.style.flexBasis = ASDimensionMake("40%")
self.rightStack.style.flexBasis = ASDimensionMake("60%")

horizontalStack.children = [self.leftStack, self.rightStack]]

ASLayoutSize

ASLayoutSize 类似于 CGSize,但是它的宽度和高度可以同时使用 pt 值或百分比值。 宽度和高度的类型是独立的,它们的值类型可以不同。

ASLayoutSizeMake(_ width: ASDimension, _ height: ASDimension)复制代码

ASLayoutSize 用于描述布局元素的 .preferredLayoutSize.minLayoutSize.maxLayoutSize 属性,它允许在一个 API 中同时使用固定值和相对值。

ASDimensionMake

ASDimension 类型 auto 表示布局元素可以根据情况选择最合理的方式。

let width = ASDimensionMake(.auto, 0)
let height = ASDimensionMake("50%")

layoutElement.style.preferredLayoutSize = ASLayoutSizeMake(width, height)

你也可以使用固定值设置布局元素的 .preferredSize.minSize.maxSize 属性。

layoutElement.style.preferredSize = CGSize(width: 30, height: 60)复制代码

大多数情况下,你不需要要限制宽度和高度。如果你需要,可以使用 ASDimension 值单独设置布局元素的 size 属性:

layoutElement.style.width     = ASDimensionMake("50%")
layoutElement.style.minWidth  = ASDimensionMake("50%")
layoutElement.style.maxWidth  = ASDimensionMake("50%")

layoutElement.style.height    = ASDimensionMake("50%")
layoutElement.style.minHeight = ASDimensionMake("50%")
layoutElement.style.maxHeight = ASDimensionMake("50%")

ASSizeRange

UIKit 没有提供一个机制绑定最小和最大的 CGSize,因此,为了支持最小和最大的 CGSize,我们创建了 ASSizeRangeASSizeRange 主要应用在 Layout API 的内部,但是 layoutSpecThatFits: 方法的的输入参数 constrainedSizeASSizeRange 类型。

func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec复制代码

传递给 ASDisplayNode 子类 layoutSpecThatFits: 方法的 constrainedSizeNode 最适合的最小和最大尺寸,你可以在布局元素上使用 constrainedSize 中包含的最小和最大 CGSize

Layout Transition API

Layout Transition API 旨在让所有的 Texture 动画都变得简单 - 甚至可以将一个视图集转为另一个完全不同的视图集!

使用这个系统,你只需指定所需的布局,Texture 会根据当前的布局自动找出差异,它会自动添加新的元素,动画结束后自动删除不需要的元素,并更新现有的元素的位置。

同时也有非常容易使用的 API,让你可以完全自定义一个新元素的起始位置,以及移除元素的结束位置。

使用 Layout Transition API 必须使用自动子节点管理功能。

布局之间的动画

Layout Transition API 使得在使用 node 制作的布局中,在 node 的内部状态更改时,可以很容易地进行动画操作。

想象一下,你希望实现这个注册的表单,并且在点击 Next 时出现新的输入框的动画:

img

实现这一点的标准方法是创建一个名为 SignupNode 的容器节点,SignupNode 包含两个可编辑的 text field node 和一个 button node 作为子节点。 我们将在 SignupNode 上包含一个名为 fieldState 的属性,该属性用于当计算布局时,要显示哪个 text field node

SignupNode 容器的内部 layoutSpec 看起来是这样的:

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
  let fieldNode: FieldNode

  if self.fieldState == .signupNodeName {
      fieldNode = self.nameField
  } else {
      fieldNode = self.ageField
  }

  let stack = ASStackLayoutSpec()
  stack.children = [fieldNode, buttonNode]

  let insets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
  return ASInsetLayoutSpec(insets: insets, child: stack)
}

为了在本例中触发从 nameFieldageField 的转换,我们将更新 SignupNode.fieldState 属性,并使用 transitionLayoutWithAnimation 方法触发动画。

这个方法将使当前计算的布局失效,并重新计算 ageField 在盒子中的布局。

self.signupNode.fieldState = .signupNodeName
self.signupNode.transitionLayout(withAnimation: true, shouldMeasureAsync: true)

在这个 API 的默认实现中,布局将重新计算,并使用它的 sublayouts 来对 SignupNode 子节点的 sizeposition 进行设置,但没有动画。这个 API 的未来版本很可能会包括布局之间的默认动画, 但现在我们需要实现一个自定义动画块来处理这个动画。

下面的示例表示在 SignupNode 中的 animateLayoutTransition: 的重写。

这个方法在通过 transitionLayoutWithAnimation: 计算出新布局之后调用,在实现中,我们将根据动画触发前设置的 fieldState 属性执行特定的动画。

override func animateLayoutTransition(_ context: ASContextTransitioning) {
  if fieldState == .signupNodeName {
    let initialNameFrame = context.initialFrame(for: ageField)

    nameField.frame = initialNameFrame
    nameField.alpha = 0

    var finalAgeFrame = context.finalFrame(for: nameField)
    finalAgeFrame.origin.x -= finalAgeFrame.size.width

    UIView.animate(withDuration: 0.4, animations: { 
        self.nameField.frame = context.finalFrame(for: self.nameField)
        self.nameField.alpha = 1
        self.ageField.frame = finalAgeFrame
        self.ageField.alpha = 0
    }, completion: { finished in
        context.completeTransition(finished)
    })
  } else {
    var initialAgeFrame = context.initialFrame(for: nameField)
    initialAgeFrame.origin.x += initialAgeFrame.size.width

    ageField.frame = initialAgeFrame
    ageField.alpha = 0

    var finalNameFrame = context.finalFrame(for: ageField)
    finalNameFrame.origin.x -= finalNameFrame.size.width

    UIView.animate(withDuration: 0.4, animations: { 
        self.ageField.frame = context.finalFrame(for: self.ageField)
        self.ageField.alpha = 1
        self.nameField.frame = finalNameFrame
        self.nameField.alpha = 0
    }, completion: { finished in
        context.completeTransition(finished)
    })
  }
}

此方法中传递的 ASContextTransitioning 上下文对象包含相关信息,可以帮助你确定转换前后的节点状态。它包括新旧约束大小,插入和删除的节点,甚至是新旧 ASLayout 原始对象。在 SignupNode 示例中,我们使用它来确定每个节点的 frame 并在一个地方让它们进动画。

一旦动画完成,就必须调用上下文对象的 completeTransition:,因为它将为新布局内部执行必要的步骤,以使新布局生效。

请注意,在这个过程中没有使用 addSubnode:removeFromSupernode:。 Layout Transition API 会分析旧布局和新布局之间节点层次结构的差异,通过自动子节点管理隐式的执行节点插入和删除。

在执行 animateLayoutTransition: 之前插入节点,这是在开始动画之前手动管理层次结构的好地方。在上下文对象执行 completeTransition :之后,清除将在 didCompleteLayoutTransition: 中执行。

如果你需要手动执行删除,请重写 didCompleteLayoutTransition: 并执行自定义的操作。需要注意的是,这样做会覆盖默认删除行为,建议你调用 super 或遍历上下文对象中的 removedSubnodes 来执行清理。

NO 传递给 transitionLayoutWithAnimation: 将贯穿 animateLayoutTransition:didCompleteLayoutTransition: 的执行,并将 [context isAnimated] 属性设置为 NO。如何处理这样的情况取决于你的选择 - 如果有的话。提供默认实现的一种简单方法是调用 super :

override func animateLayoutTransition(_ context: ASContextTransitioning) {
  if context.isAnimated() {

  } else {
      super.animateLayoutTransition(context)
  }
}
动画 constrainedSize 更改

有些时候,你只想对节点的 bounds 变化作出响应,重新计算其布局。这种情况,可以在节点上调用 transitionLayoutWithSizeRange:animated:

该方法类似于 transitionLayoutWithAnimation:,但是如果传递的 ASSizeRange 等于当前的 constrainedSizeForCalculatedLayout,则不会触发动画。 这在响应旋转事件和控制器 size 发生变化时非常有用。

官方示例代码

代码地址 https://github.com/texturegroup/texture/tree/master/examples/LayoutSpecExamples
头文件

#import 
列表

创建controller类


@interface OverviewViewController : ASDKViewController // 协议
@property (nonatomic, strong) NSArray *layoutExamples; // 数据源
@property (nonatomic, strong) ASTableNode *tableNode; // tableView
@end

@implementation OverviewViewController

#pragma mark - Lifecycle Methods

- (instancetype)init
{
  _tableNode = [ASTableNode new];
  self = [super initWithNode:_tableNode];
  
  if (self) {
    self.title = @"Layout Examples";
    self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
    
    _tableNode.delegate = self;
    _tableNode.dataSource = self;
    // 创建数据源
    _layoutExamples = @[[HeaderWithRightAndLeftItems class],
                        [PhotoWithInsetTextOverlay class],
                        [PhotoWithOutsetIconOverlay class],
                        [FlexibleSeparatorSurroundingContent class],
                        [CornerLayoutExample class],
                        [UserProfileSample class]
                        ];
  }
  
  return self;
}

- (void)viewWillAppear:(BOOL)animated
{
  [super viewWillAppear:animated];
  
  NSIndexPath *indexPath = _tableNode.indexPathForSelectedRow;
  if (indexPath != nil) {
    [_tableNode deselectRowAtIndexPath:indexPath animated:YES];
  }
}

#pragma mark - ASTableDelegate, ASTableDataSource

- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section
{
  return [_layoutExamples count];
}

// 返回cell, 因为会异步,所用用block
// typedef ASCellNode * _Nonnull(^ASCellNodeBlock)(void);
- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath {
    Class layoutExample = _layoutExamples[indexPath.row];
    return ^{
        return [[OverviewCellNode alloc] initWithLayoutExampleClass:layoutExample];
    };
}

- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  Class layoutExampleClass = [[tableNode nodeForRowAtIndexPath:indexPath] layoutExampleClass];
  LayoutExampleViewController *detail = [[LayoutExampleViewController alloc] initWithLayoutExampleClass:layoutExampleClass];
  [self.navigationController pushViewController:detail animated:YES];
}

@end

tableViewCell 类

@interface OverviewCellNode : ASCellNode

@property (nonatomic, strong) Class layoutExampleClass;

- (instancetype)initWithLayoutExampleClass:(Class)layoutExampleClass NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;

@property (nonatomic, strong) ASTextNode *titleNode;
@property (nonatomic, strong) ASTextNode *descriptionNode;

@end

@implementation OverviewCellNode

- (instancetype)initWithLayoutExampleClass:(Class)layoutExampleClass
{
    self = [super init];
    if (self) {
      self.automaticallyManagesSubnodes = YES;
      
      _layoutExampleClass = layoutExampleClass;
      
      _titleNode = [[ASTextNode alloc] init];
      _titleNode.attributedText = [NSAttributedString attributedStringWithString:[layoutExampleClass title]
                                                                  fontSize:16
                                                                     color:[UIColor blackColor]];
  
      _descriptionNode = [[ASTextNode alloc] init];
      _descriptionNode.attributedText = [NSAttributedString attributedStringWithString:[layoutExampleClass descriptionTitle]
                                                                              fontSize:12
                                                                                 color:[UIColor lightGrayColor]];
   }
    return self;
}

// 创建子结点
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
//  采用了flexbox布局
    ASStackLayoutSpec *verticalStackSpec = [ASStackLayoutSpec verticalStackLayoutSpec];
    verticalStackSpec.alignItems = ASStackLayoutAlignItemsStart;
    verticalStackSpec.spacing = 5.0;
    verticalStackSpec.children = @[self.titleNode, self.descriptionNode];
    
    return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 16, 10, 10) child:verticalStackSpec];
}

第二个controller

@interface LayoutExampleViewController : ASDKViewController
- (instancetype)initWithLayoutExampleClass:(Class)layoutExampleClass NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithNode:(ASDisplayNode *)node NS_UNAVAILABLE;

@property (nonatomic, strong) LayoutExampleNode *customNode;

@end

@implementation LayoutExampleViewController

- (instancetype)initWithLayoutExampleClass:(Class)layoutExampleClass
{
  NSAssert([layoutExampleClass isSubclassOfClass:[LayoutExampleNode class]], @"Must pass a subclass of LayoutExampleNode.");
  
  self = [super initWithNode:[ASDisplayNode new]];
  
  if (self) {
    self.title = @"Layout Example";
    
    _customNode = [layoutExampleClass new];
    [self.node addSubnode:_customNode];
    
    BOOL needsOnlyYCentering = [layoutExampleClass isEqual:[HeaderWithRightAndLeftItems class]] ||
                               [layoutExampleClass isEqual:[FlexibleSeparatorSurroundingContent class]];
                               
    self.node.backgroundColor = needsOnlyYCentering ? [UIColor lightGrayColor] : [UIColor whiteColor];
    
    __weak __typeof(self) weakself = self;
    self.node.layoutSpecBlock = ^ASLayoutSpec*(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
      return [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:needsOnlyYCentering ? ASCenterLayoutSpecCenteringY : ASCenterLayoutSpecCenteringXY
                                                        sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY
                                                                child:weakself.customNode];
      };
  }
  return self;
}
@end

六种不同的结点
@interface HeaderWithRightAndLeftItems : ASDisplayNode
@property (nonatomic, strong) ASTextNode *usernameNode;
@property (nonatomic, strong) ASTextNode *postLocationNode;
@property (nonatomic, strong) ASTextNode *postTimeNode;
@end

@implementation HeaderWithRightAndLeftItems

- (instancetype)init
{
  self = [super init];
  
  if (self) {
    self.automaticallyManagesSubnodes = YES;
    _usernameNode = [[ASTextNode alloc] init];
    _usernameNode.attributedText = [NSAttributedString attributedStringWithString:@"hannahmbanana"
                                                                         fontSize:20
                                                                            color:[UIColor darkBlueColor]];
    _usernameNode.maximumNumberOfLines = 1;
    _usernameNode.truncationMode = NSLineBreakByTruncatingTail;
    
    _postLocationNode = [[ASTextNode alloc] init];
    _postLocationNode.maximumNumberOfLines = 1;
    _postLocationNode.attributedText = [NSAttributedString attributedStringWithString:@"Sunset Beach, San Fransisco, CA"
                                                                             fontSize:20
                                                                                color:[UIColor lightBlueColor]];
    _postLocationNode.maximumNumberOfLines = 1;
    _postLocationNode.truncationMode = NSLineBreakByTruncatingTail;
    
    _postTimeNode = [[ASTextNode alloc] init];
    _postTimeNode.attributedText = [NSAttributedString attributedStringWithString:@"30m"
                                                                         fontSize:20
                                                                            color:[UIColor lightGrayColor]];
    _postLocationNode.maximumNumberOfLines = 1;
    _postLocationNode.truncationMode = NSLineBreakByTruncatingTail;
  }
  
  return self;
}

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
        // 声明一个垂直排列的盒子
  ASStackLayoutSpec *nameLocationStack = [ASStackLayoutSpec verticalStackLayoutSpec];
         // 定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小
        // 如所有元素都为 1,空间不足时,所有元素等比例缩放
        // 如其中一个是 0,则此元素不缩放,其他元素均分剩余空间
  nameLocationStack.style.flexShrink = 1.0;
       // 定义元素的放大比例,默认为 0,即如果存在剩余空间,也不放大
        // 如所有元素都为 1,均分剩余空间
        // 如其中一个为 2,那么这个元素占据的空间是其他元素的一倍
  nameLocationStack.style.flexGrow = 1.0;
          // 根据定位地址 node 是否赋值,确定是否将其加入视图
  if (_postLocationNode.attributedText) {
    nameLocationStack.children = @[_usernameNode, _postLocationNode];
  } else {
    nameLocationStack.children = @[_usernameNode];
  }
  
        // 声明一个水平排列的盒子
        // direction: .horizontal 主轴是水平的
        // spacing: 40 其子元素的间距是 40
        // justifyContent: .start 在主轴上从左至右排列
        // alignItems: .center 在次轴也就是垂直轴中居中
        // children: [nameLoctionStack, postTimeNode] 包含的子元素
  ASStackLayoutSpec *headerStackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
                                                                               spacing:40
                                                                        justifyContent:ASStackLayoutJustifyContentStart
                                                                            alignItems:ASStackLayoutAlignItemsCenter
                                                                              children:@[nameLocationStack, _postTimeNode]];
          // 插入布局规则
  return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(0, 10, 0, 10) child:headerStackSpec];
}

@end
image.png
@interface PhotoWithInsetTextOverlay : ASDisplayNode
@property (nonatomic, strong) ASNetworkImageNode *photoNode;
@property (nonatomic, strong) ASTextNode *titleNode;
@end
@implementation PhotoWithInsetTextOverlay

- (instancetype)init
{
  self = [super init];
  
  if (self) {
    self.automaticallyManagesSubnodes = YES;
    self.backgroundColor = [UIColor clearColor];
    
    _photoNode = [[ASNetworkImageNode alloc] init];
    _photoNode.URL = [NSURL URLWithString:@"http://texturegroup.org/static/images/layout-examples-photo-with-inset-text-overlay-photo.png"];
    _photoNode.willDisplayNodeContentWithRenderingContext = ^(CGContextRef context, id drawParameters) {
      CGRect bounds = CGContextGetClipBoundingBox(context);
      [[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:10] addClip];
    };
    
    _titleNode = [[ASTextNode alloc] init];
    _titleNode.maximumNumberOfLines = 2;
    _titleNode.truncationMode = NSLineBreakByTruncatingTail;
    _titleNode.truncationAttributedText = [NSAttributedString attributedStringWithString:@"..." fontSize:16 color:[UIColor whiteColor]];
    _titleNode.attributedText = [NSAttributedString attributedStringWithString:@"family fall hikes" fontSize:16 color:[UIColor whiteColor]];
  }
  
  return self;
}

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
  CGFloat photoDimension = constrainedSize.max.width / 4.0;
  _photoNode.style.preferredSize = CGSizeMake(photoDimension, photoDimension);

  // INFINITY 设定 titleNode 上边距无限大
  UIEdgeInsets insets = UIEdgeInsetsMake(INFINITY, 12, 12, 12);
  ASInsetLayoutSpec *textInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:_titleNode];
  
  return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:_photoNode overlay:textInsetSpec];;
}

@end
image.png
@interface PhotoWithOutsetIconOverlay : ASDisplayNode
@property (nonatomic, strong) ASNetworkImageNode *photoNode;
@property (nonatomic, strong) ASNetworkImageNode *iconNode;
@end
@implementation PhotoWithOutsetIconOverlay
- (instancetype)init
{
  self = [super init];
  
  if (self) {
    self.automaticallyManagesSubnodes = YES;
    _photoNode = [[ASNetworkImageNode alloc] init];
    _photoNode.URL = [NSURL URLWithString:@"http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-photo.png"];
    
    _iconNode = [[ASNetworkImageNode alloc] init];
    _iconNode.URL = [NSURL URLWithString:@"http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-icon.png"];
    
    [_iconNode setImageModificationBlock:^UIImage *(UIImage *image, ASPrimitiveTraitCollection traitCollection) {   // FIXME: in framework autocomplete for setImageModificationBlock line seems broken
      CGSize profileImageSize = CGSizeMake(60, 60);
      return [image makeCircularImageWithSize:profileImageSize withBorderWidth:10];
    }];
  }
  
  return self;
}

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
// 设置大小和位置
  _iconNode.style.preferredSize = CGSizeMake(40, 40);
  _iconNode.style.layoutPosition = CGPointMake(150, 0);
  
  _photoNode.style.preferredSize = CGSizeMake(150, 150);
  _photoNode.style.layoutPosition = CGPointMake(40 / 2.0, 40 / 2.0);
  
  ASAbsoluteLayoutSpec *absoluteSpec = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[_photoNode, _iconNode]];
  
  // 大小自适应
  absoluteSpec.sizing = ASAbsoluteLayoutSpecSizingSizeToFit;
  
  return absoluteSpec;
}

@end
image.png
@interface FlexibleSeparatorSurroundingContent : ASDisplayNode
@property (nonatomic, strong) ASImageNode *topSeparator;
@property (nonatomic, strong) ASImageNode *bottomSeparator;
@property (nonatomic, strong) ASTextNode *textNode;
@end
@implementation FlexibleSeparatorSurroundingContent

- (instancetype)init
{
  self = [super init];
  
  if (self) {
    self.automaticallyManagesSubnodes = YES;
    self.backgroundColor = [UIColor whiteColor];

    _topSeparator = [[ASImageNode alloc] init];
    _topSeparator.image = [UIImage as_resizableRoundedImageWithCornerRadius:1.0
                                                                cornerColor:[UIColor blackColor]
                                                                  fillColor:[UIColor blackColor]
                                                            traitCollection:self.primitiveTraitCollection];
    
    _textNode = [[ASTextNode alloc] init];
    _textNode.attributedText = [NSAttributedString attributedStringWithString:@"this is a long text node"
                                                                     fontSize:16
                                                                        color:[UIColor blackColor]];
    
    _bottomSeparator = [[ASImageNode alloc] init];
    _bottomSeparator.image = [UIImage as_resizableRoundedImageWithCornerRadius:1.0
                                                                   cornerColor:[UIColor blackColor]
                                                                     fillColor:[UIColor blackColor]
                                                               traitCollection:self.primitiveTraitCollection];
  }
  
  return self;
}

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{

// 放大比例
  _topSeparator.style.flexGrow = 1.0;
  _bottomSeparator.style.flexGrow = 1.0;
// 中心
  _textNode.style.alignSelf = ASStackLayoutAlignSelfCenter;

  ASStackLayoutSpec *verticalStackSpec = [ASStackLayoutSpec verticalStackLayoutSpec];
//   间距
  verticalStackSpec.spacing = 20;
  verticalStackSpec.justifyContent = ASStackLayoutJustifyContentCenter;
  verticalStackSpec.children = @[_topSeparator, _textNode, _bottomSeparator];

  return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(60, 0, 60, 0) child:verticalStackSpec];
}

@end
image.png
@interface CornerLayoutExample : PhotoWithOutsetIconOverlay
@property (nonatomic, strong) ASImageNode *dotNode;
@property (nonatomic, strong) ASImageNode *photoNode1;
@property (nonatomic, strong) ASTextNode *badgeTextNode;
@property (nonatomic, strong) ASImageNode *badgeImageNode;
@property (nonatomic, strong) ASImageNode *photoNode2;
@end
@implementation CornerLayoutExample

static CGFloat const kSampleAvatarSize = 100;
static CGFloat const kSampleIconSize = 26;
static CGFloat const kSampleBadgeCornerRadius = 12;

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.automaticallyManagesSubnodes = YES;

        UIImage *avatarImage = [self avatarImageWithSize:CGSizeMake(kSampleAvatarSize, kSampleAvatarSize)];
        UIImage *cornerImage = [self cornerImageWithSize:CGSizeMake(kSampleIconSize, kSampleIconSize)];
        
        NSAttributedString *numberText = [NSAttributedString attributedStringWithString:@" 999+ " fontSize:20 color:UIColor.whiteColor];
        
        _dotNode = [ASImageNode new];
        _dotNode.image = cornerImage;
        
        _photoNode1 = [ASImageNode new];
        _photoNode1.image = avatarImage;
        
        _badgeTextNode = [ASTextNode new];
        _badgeTextNode.attributedText = numberText;
        
        _badgeImageNode = [ASImageNode new];
        _badgeImageNode.image = [UIImage as_resizableRoundedImageWithCornerRadius:kSampleBadgeCornerRadius
                                                                      cornerColor:UIColor.clearColor
                                                                        fillColor:UIColor.redColor
                                                                  traitCollection:self.primitiveTraitCollection];
        
        _photoNode2 = [ASImageNode new];
        _photoNode2.image = avatarImage;
    }
    return self;
}

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
    
    ASBackgroundLayoutSpec *badgeSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:_badgeTextNode
                                                                                   background:_badgeImageNode];
    
    ASCornerLayoutSpec *cornerSpec1 = [ASCornerLayoutSpec cornerLayoutSpecWithChild:_photoNode1 corner:_dotNode location:ASCornerLayoutLocationTopRight];
    cornerSpec1.offset = CGPointMake(-3, 3);
    
    ASCornerLayoutSpec *cornerSpec2 = [ASCornerLayoutSpec cornerLayoutSpecWithChild:_photoNode2 corner:badgeSpec location:ASCornerLayoutLocationTopRight];
    
    self.photoNode.style.preferredSize = CGSizeMake(kSampleAvatarSize, kSampleAvatarSize);
    self.iconNode.style.preferredSize = CGSizeMake(kSampleIconSize, kSampleIconSize);
    
    ASCornerLayoutSpec *cornerSpec3 = [ASCornerLayoutSpec cornerLayoutSpecWithChild:self.photoNode corner:self.iconNode location:ASCornerLayoutLocationTopRight];
    
    ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec verticalStackLayoutSpec];
    stackSpec.spacing = 40;
    stackSpec.children = @[cornerSpec1, cornerSpec2, cornerSpec3];
    
    return stackSpec;
}

- (UIImage *)avatarImageWithSize:(CGSize)size
{
    return [UIImage imageWithSize:size fillColor:UIColor.lightGrayColor shapeBlock:^UIBezierPath *{
        CGRect rect = (CGRect){ CGPointZero, size };
        return [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:MIN(size.width, size.height) / 20];
    }];
}

- (UIImage *)cornerImageWithSize:(CGSize)size
{
    return [UIImage imageWithSize:size fillColor:UIColor.redColor shapeBlock:^UIBezierPath *{
        return [UIBezierPath bezierPathWithOvalInRect:(CGRect){ CGPointZero, size }];
    }];
}

@end
image.png
@interface UserProfileSample : ASDisplayNode
@property (nonatomic, strong) ASImageNode *badgeNode;
@property (nonatomic, strong) ASImageNode *avatarNode;
@property (nonatomic, strong) ASTextNode *usernameNode;
@property (nonatomic, strong) ASTextNode *subtitleNode;
@property (nonatomic, assign) CGFloat photoSizeValue;
@property (nonatomic, assign) CGFloat iconSizeValue;
@end
@implementation UserProfileSample

- (instancetype)init
{
    self = [super init];
    if (self) {
      self.automaticallyManagesSubnodes = YES;
        _photoSizeValue = 44;
        _iconSizeValue = 15;
        
        CGSize iconSize = CGSizeMake(_iconSizeValue, _iconSizeValue);
        CGSize photoSize = CGSizeMake(_photoSizeValue, _photoSizeValue);
        
        _badgeNode = [ASImageNode new];
        _badgeNode.style.preferredSize = iconSize;
        _badgeNode.image = [UIImage imageWithSize:iconSize fillColor:UIColor.redColor shapeBlock:^UIBezierPath *{
            return [UIBezierPath bezierPathWithOvalInRect:(CGRect){ CGPointZero, iconSize }];
        }];
        
        _avatarNode = [ASImageNode new];
        _avatarNode.style.preferredSize = photoSize;
        _avatarNode.image = [UIImage imageWithSize:photoSize fillColor:UIColor.lightGrayColor shapeBlock:^UIBezierPath *{
            return [UIBezierPath bezierPathWithOvalInRect:(CGRect){ CGPointZero, photoSize }];
        }];
        
        _usernameNode = [ASTextNode new];
        _usernameNode.attributedText = [NSAttributedString attributedStringWithString:@"Hello World" fontSize:17 color:UIColor.blackColor];
        _usernameNode.maximumNumberOfLines = 1;
        
        _subtitleNode = [ASTextNode new];
        _subtitleNode.attributedText = [NSAttributedString attributedStringWithString:@"This is a long long subtitle, with a long long appended string." fontSize:14 color:UIColor.lightGrayColor];
        _subtitleNode.maximumNumberOfLines = 1;
    }
    return self;
}

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
    // Apply avatar with badge
    // Normally, avatar's box size is the only photo size and it will not include the badge size.
    // Otherwise, use includeCornerForSizeCalculation property to increase the box's size if needed.
    ASCornerLayoutSpec *avatarBox = [ASCornerLayoutSpec new];
    avatarBox.child = _avatarNode;
    avatarBox.corner = _badgeNode;
    avatarBox.cornerLocation = ASCornerLayoutLocationBottomRight;
    avatarBox.offset = CGPointMake(-6, -6);
    
    ASStackLayoutSpec *textBox = [ASStackLayoutSpec verticalStackLayoutSpec];
    textBox.justifyContent = ASStackLayoutJustifyContentSpaceAround;
    textBox.children = @[_usernameNode, _subtitleNode];
    
    ASStackLayoutSpec *profileBox = [ASStackLayoutSpec horizontalStackLayoutSpec];
    profileBox.spacing = 10;
    profileBox.children = @[avatarBox, textBox];
    
    // Apply text truncation.
    NSArray *elems = @[_usernameNode, _subtitleNode, textBox, profileBox];
    for (id  elem in elems) {
        elem.style.flexShrink = 1;
    }
    
    ASInsetLayoutSpec *profileInsetBox = [ASInsetLayoutSpec new];
    profileInsetBox.insets = UIEdgeInsetsMake(120, 20, INFINITY, 20);
    profileInsetBox.child = profileBox;
    
    return profileInsetBox;
}
@end
image.png

ASStackLayoutSpec 示例代码

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constraint
{
    // 创建一个纵轴方向的 ASStackLayoutSpec 视图容器 vStack
    ASStackLayoutSpec *vStack = [[ASStackLayoutSpec alloc] init];
    // 设置两个子节点,第一个节点是标题,第二个正文内容
    [vStack setChildren:@[titleNode, bodyNode];

    // 创建一个横轴方向的 ASStackLayoutSpec 视图容器 hstack
    ASStackLayoutSpec *hstack = [[ASStackLayoutSpec alloc] init];
    hStack.direction          = ASStackLayoutDirectionHorizontal;
    hStack.spacing            = 5.0; // 设置节点间距为5

    // 在 hStack 里添加 imageNode 和 vStack 节点
    [hStack setChildren:@[imageNode, vStack]];
    
    // 创建一个 ASInsetLayoutSpec 容器,设置四周边距为5,将 hStack 作为其子节点
    ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(5,5,5,5) child:hStack];

    return insetSpec;
}

会先创建一个纵轴方向的 ASStackLayoutSpec 视图容器 vStack;为 vStack 设置两个子节点,第一个节点是标题,第二个节点是正文内容;

接下来,创建一个横轴方向的 ASStackLayoutSpec 视图容器 hstack,在 hStack 里添加 imageNode 和 vStack 节点;

最后,创建一个 ASInsetLayoutSpec 容器,设置四周边距为 5,将 hStack 作为其子节点。

image.png

使用实例:
https://github.com/texturegroup/texture/tree/master/examples
https://github.com/texturegroup/texture/tree/master/examples/ASDKgram

你可能感兴趣的:(Flexbox和Texture)