[译] AsyncDisplayKit/Texture 官方文档(2)

6,338 阅读23分钟

官方文档链接:texturegroup.org/docs/gettin…

布局快速入门

[译] AsyncDisplayKit/Texture 官方文档(1)

开发初衷和优势

Layout API 的出现是为了提供一种可以替代 UIKit Auto Layout 的高性能方案,UIKit Auto Layout 在复杂的视图结构中,计算量会呈指数级增长,Texture 的布局方案相对 Auto Layout 有以下优点:

  • 快:Texture 的布局计算和手写 Frame 一样的快;
  • 异步和并发:布局可以在后台线程上计算,用户交互不会因此而中断;
  • 声明式渲染:布局使用不可变的数据结构声明,这让布局代码变得更容易开发、维护、调试、测试和评审;
  • 可缓存:如果布局是不可变的,可以在后台预先计算并缓存,这可以让用户感觉更快;
  • 可扩展:在不同的类中使用相同的布局会变得很方便;

灵感来自于 CSS Flexbox

熟悉 Flexbox 的人会注意到这两个系统有许多的相似之处, 但 Layout API 并没有重新实现所有的 CSS。

基本概念

Texture 的布局主要围绕两个概念展开:

  1. 布局规则
  2. 布局元素

布局规则/Layout Specs

布局规则没有物理存在,它通过充当 LayoutElements 的容器,理解多个 LayoutElements 之间的关联,完成 LayoutElements 的位置排列。Texture 提供了 ASLayoutSpec 的几个子类,涵盖了从插入单个布局元素的简单规则,到可以变化堆放排列配置,包含多个布局元素的复杂规则。

布局元素/Layout Elements

LayoutSpecs 包含 LayoutElements,并对 LayoutElements 进行整理。

所有的 ASDisplayNodeASLayoutSpec 都遵守 <ASLayoutElement> 协议,这意味着你可以通过两个 Nodes 和其他的 LayoutSpecs,生成或者组合一个新的 LayoutSpecs

<ASLayoutElement> 协议有一些属性用于创建非常复杂的布局。 另外,LayoutSpecs 也有自己的一组属性,可以调整布局元素的排列。

结合布局规则和布局元素制作复杂界面

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

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

一些 Node 需要设定 size

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

  • ASImageNode
  • ASTextNode
  • ASButtonNode

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

  • ASVideoNode
  • ASVideoPlayerNode
  • ASNetworkImageNode
  • ASEditableTextNode

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

布局调试/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}

布局示例

点击查看layoutSpec示例工程

简单的文本左对齐和右对齐

为了创建这个一个布局,我们将使用:

  • 表示垂直的 ASStackLayoutSpec
  • 表示水平的 ASStackLayoutSpec
  • 插入标题的 ASInsetLayoutSpec

下图展示了一个由 NodeLayoutSpecs 组成的布局元素:

class TZYVC: ASViewController<ASDisplayNode> {
    init() {
        let node = TZYNode()
        super.init(node: node)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        node.backgroundColor = UIColor.red
    }
}

///////////////////////////////////////////////////

class TZYNode: ASDisplayNode {

    // 图中的 san fran ca
    lazy var postLocationNode: ASTextNode = {
        return ASTextNode()
    }()

    // 图中的 hannahmbanana
    lazy var userNameNode: ASTextNode = {
        return ASTextNode()
    }()

    // 图中的 30m
    lazy var postTimeNode: ASTextNode = {
        return ASTextNode()
    }()

    override init() {
        super.init()
        self.postLocationNode.attributedText = NSAttributedString(string: "san fran ca")
        self.userNameNode.attributedText = NSAttributedString(string: "hannahmbanana")
        self.postTimeNode.attributedText = NSAttributedString(string: "30m")
        addSubnode(postLocationNode)
        addSubnode(userNameNode)
        addSubnode(postTimeNode)
        postTimeNode.backgroundColor = .brown
        userNameNode.backgroundColor = .cyan
        postLocationNode.backgroundColor = .green
    }

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

将示例项目从纵向转换为横向,查看间隔是如何增长和收缩的。

图像上覆盖文本

要创建这个布局,我们将使用:

  • 用于插入文本的 ASInsetLayoutSpec
  • 将文本覆盖到图片的 ASOverlayLayoutSpec
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
  let photoDimension: CGFloat = constrainedSize.max.width / 4.0
  photoNode.style.preferredSize = CGSize(width: photoDimension, height: photoDimension)
  // CGFloat.infinity 设定 titleNode 上边距无限大
  let insets = UIEdgeInsets(top: CGFloat.infinity, left: 12, bottom: 12, right: 12)
  let textInsetSpec = ASInsetLayoutSpec(insets: insets, child: titleNode)
  return ASOverlayLayoutSpec(child: photoNode, overlay: textInsetSpec)
}

图片上覆盖图标

要创建这个布局,我们将用到:

  • 设定 sizepositionASLayoutable 属性;
  • 用于放置图片和图标的 ASAbsoluteLayoutSpec
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
  iconNode.style.preferredSize = CGSize(width: 40, height: 40);
  iconNode.style.layoutPosition = CGPoint(x: 150, y: 0);
  photoNode.style.preferredSize = CGSize(width: 150, height: 150);
  photoNode.style.layoutPosition = CGPoint(x: 40 / 2.0, y: 40 / 2.0);
  let absoluteSpec = ASAbsoluteLayoutSpec(children: [photoNode, iconNode])
  // ASAbsoluteLayoutSpec 的 sizing 属性重新创建了 Texture Layout API 1.0 中的 ASStaticLayoutSpec
  absoluteSpec.sizing = .sizeToFit
  return absoluteSpec;
}

简单的插入文本单元格

要创建一个类似 Pinterest 搜索视图的单一单元格布局,我们将用到:

  • 用于插入文本的 ASInsetLayoutSpec
  • 根据指定的属性将文本居中的 ASCenterLayoutSpec
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    let insets = UIEdgeInsets(top: 0, left: 12, bottom: 4, right: 4)
    let inset = ASInsetLayoutSpec(insets: insets, child: _titleNode)
    return ASCenterLayoutSpec(centeringOptions: .Y, sizingOptions: .minimumX, child: inset)
}

顶部和底部的分割线

创建一个如上的布局,我们需要用到:

  • 用于插入文本的 ASInsetLayoutSpec
  • 用于在文本的顶部和底部添加分隔线,垂直的 ASStackLayoutSpec

下图展示了一个 layoutables是如何通过 layoutSpecsNode 组成的:

以下的代码也可以在 ASLayoutSpecPlayground 这个示例项目中找到。

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
  topSeparator.style.flexGrow = 1.0
  bottomSeparator.style.flexGrow = 1.0
  textNode.style.alignSelf = .center
  let verticalStackSpec = ASStackLayoutSpec.vertical()
  verticalStackSpec.spacing = 20
  verticalStackSpec.justifyContent = .center
  verticalStackSpec.children = [topSeparator, textNode, bottomSeparator]
  return ASInsetLayoutSpec(insets:UIEdgeInsets(top: 60, left: 0, bottom: 60, right: 0), child: verticalStackSpec)
}

布局规则/Layout Specs

以下的 ASLayoutSpec 子类可以用来组成简单或者非常复杂的布局:

规则 描述
ASWrapperLayoutSpec 填充布局
ASStackLayoutSpec 盒子布局
ASInsetLayoutSpec 插入布局
ASOverlayLayoutSpec 覆盖布局
ASBackgroundLayoutSpec 背景布局
ASCenterLayoutSpec 中心布局
ASRatioLayoutSpec 比例布局
ASRelativeLayoutSpec 顶点布局
ASAbsoluteLayoutSpec 绝对布局

你也可以创建一个 ASLayoutSpec 的子类以制作自己的布局规则。

ASWrapperLayoutSpec

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 (Flexbox Container)

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

除了 ASLayoutElement 属性,ASStackLayoutSpec 还有 7 个属性:

  • direction

    指定子元素的排序方向,如果设置了 horizontalAlignmentverticalAlignment,它们将被再次解析,这会导致 justifyContentalignItems 也会相应地更新。

  • spacing

    描述子元素之间的距离

  • horizontalAlignment

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

  • verticalAlignment

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

  • justifyContent

    描述子元素之间的距离。

  • alignItems

    描述子元素在十字轴上的方向。

spacing 和 justifyContent 原文都是 The amount of space between each child.

spacing 以我的理解应该翻译的没错,但是 justifyContent 感觉不太准确,这几个属性读者可以查阅 CSS 文档自行理解。

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
}

Flexbox 在 Web 上的工作方式与在 CSS 中的工作方式相同,单有一些例外。例如,默认值是不同的,没有 flex 参数,有关更多信息,请参阅 Web Flexbox 差异

ASInsetLayoutSpec

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

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

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
  ...
  let insets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
  let headerWithInset = ASInsetLayoutSpec(insets: insets, child: textNode)
  ...
}

如果在你将 UIEdgeInsets 中的一个值设置为 INFINITY,则 insetSpec 将只使用子节点的固有大小,请看 图像上覆盖文本 这个例子。

ASOverlayLayoutSpec

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

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

当使用 ASOverlayLayoutSpec 进行自动的子节点管理时,节点有时会表现出错误的顺序,这是一个已知的问题,并且很快就会解决。当前的解决方法是手动添加节点,布局元素(红色)必须作为子节点添加到父节点后面的子节点(蓝色)。

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

当使用 ASOverlayLayoutSpec 进行自动的子节点管理时,节点有时会表现出错误的顺序,这是一个已知的问题,并且很快就会解决。当前的解决方法是手动添加节点,布局元素(蓝色)必须作为子节点添加到父节点后面的子节点(红色)。

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

注意:添加子节点的顺序对于这个布局规则是很重要的。 背景对象必须在前台对象之前作为子节点添加到父节点,目前使用 ASM 不能保证这个顺序一定是正确的!

ASCenterLayoutSpec

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

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

ASCenterLayoutSpec 有两个属性:

  • centeringOptions:

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

  • sizingOptions:

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

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 提供固有大小是非常常见的,因为两者在内容从服务器返回之前都没有固有大小。

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)
  ...
}

ASAbsoluteLayoutSpec

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])
}

ASLayoutSpec

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]
  ...
}

布局元素属性/Layout Element Properties

  • ASStackLayoutElement Properties:只会在盒子布局中的的 subnodelayoutSpec 中生效;
  • ASAbsoluteLayoutElement Properties:只会在绝对布局中的的 subnodelayoutSpec 中生效;
  • ASLayoutElement Properties:适用于所有 NodelayoutSpec

ASStackLayoutElement Properties

请注意,以下属性只有在 ASStackLayoutsubnode上设置才会生效。

.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

请注意,以下属性只有在 AbsoluteLayoutsubnode上设置才会生效。

.style.layoutPosition

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

ASLayoutElement Properties

请注意,以下属性适用于所有布局元素。

.style.width

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

.style.height

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

.style.minWidth

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

.style.maxWidth

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

.style.minHeight

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

.style.maxHeight

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

.style.preferredSize

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

此方法是可选的,但是对于没有固有大小或需要用与固有大小不同的的 size 进行布局的节点,则必须指定 preferredSizepreferredLayoutSize 中的一个,比如没这个属性可以在 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 类型,为布局元素提供建议的相对 sizeASLayoutSize 使用百分比而不是点来指定布局。 例如,子布局元素的宽度应该是父宽度的 50%。 如果提供了可选的 minLayoutSizemaxLayoutSize,并且 preferredLayoutSize 超过了这些值,则将使用 minLayoutSizemaxLayoutSize。 如果未提供此可选值,则布局元素的 size 将默认是 calculateSizeThatFits: 提供的固有大小。

.style.minLayoutSize

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

.style.maxLayoutSize

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

Layout API Sizing

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

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]]

CGSize、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 主要应用在 Llayout 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 时出现新的输入框的动画:

实现这一点的标准方法是创建一个名为 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 发生变化时非常有用。

使用 Layout Transition API 的示例

未完待续