Texture 布局

文章目录
  1. 1. Texture 目标&优点
  2. 2. 基本概念
    1. 2.1. Layout Specs
    2. 2.2. Layout Elements
  3. 3. Layout Specs 子类
    1. 3.1. ASWrapperLayoutSpec
    2. 3.2. ASInsetLayoutSpec
    3. 3.3. ASOverlayLayoutSpec
    4. 3.4. ASBackgroundLayoutSpec
    5. 3.5. ASCenterLayoutSpec
    6. 3.6. ASRatioLayoutSpec
    7. 3.7. ASRelativeLayoutSpec
    8. 3.8. ASAbsoluteLayoutSpec
    9. 3.9. ASCornerLayoutSpec
    10. 3.10. ASLayoutSpec
    11. 3.11. ASStackLayoutSpec(Flexbox Container)
  4. 4. Layout Element 属性
    1. 4.1. ASStackLayoutElement(flex-item)属性
    2. 4.2. ASAbsoluteLayoutElement 属性
    3. 4.3. ASLayoutElement 属性
  5. 5. Layout API Sizing
    1. 5.1. Values (CGFloat, ASDimension)
    2. 5.2. 使用 ASDimension 的例子
    3. 5.3. Sizes (CGSize, ASLayoutSize)
    4. 5.4. Size Range (ASSizeRange)

Texture 目标&优点

Texture 的 Layout API是 UIKit AutoLayout 的高效替代品

  • Fast:自动布局比 AutoLayout 快的多
  • Asynchronous & Concurrent: Layout 在后台计算,所以用户交互不会被终端
  • Declarative:布局用不可变的数据结构声明。这使布局代码更易于开发,文档编制,代码审查,测试,调试,配置文件和维护。
  • Cacheable:布局结果是不可变的数据结构,因此可以在后台对其进行预先计算并进行缓存以提高用户的感知性能。
  • Extensible:易于在类之间共享代码。

受 CSS Flexbox 模型启发flexbox model

基本概念

Texture 布局系统的两个核心概念:

  1. Layout Specs
  2. Layout Elements

Layout Specs

class ASLayoutSpec
布局规范(layout specification)通过了解这些子布局元素之间的相互关系布局元素,是布局元素的容器。

Layout Elements

protocol <ASLayoutElement>
Layout Specs 包含并排列 Layout Elements
所有 ASDisplayNodes & ASLayoutSpecs 都遵循协议

1
2
3
4
5
6
-----------------------ASStackLayoutSpec----------------------
| -----ASStackLayoutSpec----- -----ASStackLayoutSpec----- |
| | ASImageNode | | ASImageNode | |
| | ASImageNode | | ASImageNode | |
| --------------------------- --------------------------- |
--------------------------------------------------------------

Layout Specs 子类

  • ASWrapperLayoutSpec
  • ASStackLayoutSpec
  • ASInsetLayoutSpec
  • ASOverlayLayoutSpec
  • ASBackgroundLayoutSpec
  • ASCenterLayoutSpec
  • ASRatioLayoutSpec
  • ASRelativeLayoutSpec
  • ASAbsoluteLayoutSpec
  • ASCornerLayoutSpec

ASWrapperLayoutSpec

它可以包裹一个 ASLayoutElement ,通过这个 element 的siez set 计算这个 element 的layout。

用于:
ASWrapperLayoutSpec 对于在 -layoutSpecThatFits: 中包裹单个 node,返回spec.

1
2
3
4
5
6
7
8
9
10
11
// return a single subnode from layoutSpecThatFits:
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
return ASWrapperLayoutSpec(layoutElement: _subnode)
}

// set a size (but not 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)
}

ASInsetLayoutSpec

给 element 添加 inset margin。element 必须有 instrinsic size or 显示设置的size

runloop
1
2
3
4
5
6
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)
return headerWithInset
}

ASOverlayLayoutSpec

重叠布局规范尺寸是根据 element 的尺寸计算得出的。在下图中,element 是蓝色层
重叠布局规范尺寸是根据包裹 element 的尺寸计算出来的。在下图中,子元素是蓝色图层。蓝色 element 会把他的 size 作为 constrainedSize 传给重叠布局元素(红色图层)。所以 element (蓝色layer)必须要有自己的 intrinsic size 或者 已经设置 size了。

OverlayLayout
1
2
3
4
5
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec{
let backgroundNode = ASDisplayNodeWithBackgroundColor(UIColor.blue)
let foregroundNode = ASDisplayNodeWithBackgroundColor(UIColor.red)
return ASOverlayLayoutSpec(child: backgroundNode, overlay: foregroundNode)
}

ASBackgroundLayoutSpec

ASBackgroundLayoutSpec 布局一个组件(蓝色),并拉伸其后的另一个组件作为背景(红色)。

background 规范 size根据 child 的尺寸计算。下图中,蓝色是child。将 blue child 的size 当做 constrainedSize 传给 background layout element (red layer). so child(blue layer) 一定要有自己的intrinsic size 或者 已经设置 size了。

Texture-BackgroundLayout
1
2
3
4
5
6
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 将放在 child 中心位置。
如果center spec’s width 和 height 没有限制,那么 ASCenterLayoutSpec 缩到 child 的size大小。

ASCenterLayoutSpec 两个属性:

  • centeringOptions. 确定 child 如何在中心规格内居中。选项包括:None, X, Y, XY.
  • sizingOptions. 确定中心规格将占用多少空间。选项包括:Default, minimum X, minimum Y, minimum XY.
1
2
3
4
5
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 使用固定宽高比来布局组件。ASRatioLayoutSpec 他的 constrainedSize 必须要配置 width or height。这样才能够使用 scale

ASRatioLayoutSpec 经常用于那些没有 intrinsic size 的node(ASNetworkImageNode or ASVideoNode)。

1
2
3
4
5
6
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
// Half Ratio
let subnode = ASDisplayNodeWithBackgroundColor(UIColor.green, CGSize(width: 100, height: 100.0))
let ratioSpec = ASRatioLayoutSpec(ratio: 0.5, child: subnode)
return ratioSpec
}

ASRelativeLayoutSpec

并根据垂直和水平位置说明符对组件进行布局.
与“ 9部分”图像区域相似,可以将孩子放置在4个角的任意一个或4个边缘中的任意一个的中间以及中心。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

通过设置其子节点的layoutPosition属性来指定其子节点的确切位置(x / y坐标)。
绝对布局比其他类型的布局更不灵活且难以维护。

唯一属性sizing: 确定 ASAbsoluteLayoutSpec 占用多少空间。选项包括:Default 和 Size to Fit。请注意,Size to Fit 选项将复制旧的ASStaticLayoutSpec的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let maxConstrainedSize = constrainedSize.max

// Layout all nodes absolute in a static layout spec
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])
}

ASCornerLayoutSpec

ASCornerLayoutSpec 提供一个简单方法,把 element 放在角落。

Texture-BackgroundLayout
1
2
3
4
5
6
7
8
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec{
...
// Layout the center of badge to the top right corner of avatar.
let cornerSpec = ASCornerLayoutSpec(child: avatarNode, corner: badgeNode, location: .topRight)
// Slightly shift center of badge inside of avatar.
cornerSpec.offset = CGPoint(x: -3, y: 3)
...
}

ASLayoutSpec

  1. 所有 layout spec 都继承自它
  2. 主要任务:处理 children 布局管理,用户可以 子类化它实现自己自定义 layout spec。
  3. 作为间隔区:充当 ASStackLayoutSpec 的spacer,当使用 .flexGrow and/or .flexShrink 时候
1
2
3
4
5
6
7
8
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
...
let spacer = ASLayoutSpec()
spacer.style.flexGrow = 1.0

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

ASStackLayoutSpec(Flexbox Container)

相关属性对照flexbox-model

有 7 个属性:

  • direction. 确定 stack 方向。如果已经设置了 horizontalAlignment and verticalAlignment, 他们会被重新解析一遍。导致 justifyContent and alignItems也会相应的变化。
  • spacing. 每个 child element 之间的距离.
  • horizontalAlignment. 指定 children 是如何水平对齐的。根据stack direction方向。设置对齐方式 会导致 justifyContent or alignItems 更新。
  • verticalAlignment. 指定 children 是如何垂直对齐的。根据stack direction方向。设置对齐方式 会导致 justifyContent or alignItems 更新。
  • justifyContent. 沿主轴的对齐方式.
  • alignItems. 沿交叉轴的方向布局.
  • flexWrap. element 堆叠成单行还是多行。默认为单行。
  • alignContent. 元素组成有多行,cross-axis方向空间充足,内容行的对齐方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let mainStack = ASStackLayoutSpec(direction: .horizontal,
spacing: 6.0,
justifyContent: .start,
alignItems: .center,
children: [titleNode, subtitleNode])

// Set some constrained size to the stack
mainStack.style.minWidth = ASDimensionMakeWithPoints(60.0)
mainStack.style.maxHeight = ASDimensionMakeWithPoints(40.0)

return mainStack
}

Layout Element 属性

  • ASStackLayoutElement Properties:只会在 StackLayoutSpec 中的元素(node or layout spec)生效
  • ASAbsoluteLayoutElement Properties:只会在AbsoluateLayoutSpec中的元素( subnode 或 layoutSpec)生效;
  • ASLayoutElement Properties:适用于所有 Node 和 layoutSpec;

ASStackLayoutElement(flex-item)属性

相关属性对照flexbox-model

属性类型描述
.style.spacingBeforeCGFloatdirection 上与前一个 node 的间隔
.style.spacingAfterCGFloatdirection 上与后一个 node 的间隔
.style.flexGrowBool子节点尺寸总和小于 minimum即存在剩余空间时,是否放大
.style.flexShrinkBool子节点总和大于 maximum,即空间不足时,是否缩小
.style.flexBasisASDimension在使用flexGrow 或 flexShrink 属性之前,并且剩余空间被均分之前,指定item的初始size
.style.alignSelfASStackLayoutAlignSelfitem 在cross-axis方向布局方式,此属性会覆盖 layoutSpec(flex-container) 属性alignItems,可选值有:Auto、Start、End、Center、Stretch
.style.ascenderCGFloat用于基线对齐,描述对象从顶部到其基线的距离
.style.descenderCGFloat用于基线对齐,描述对象从基线到其底部的距离

ASAbsoluteLayoutElement 属性

属性类型描述
.style.layoutPositionCGPoint该对象在 ASAbsoluteLayoutSpec 中的位置

ASLayoutElement 属性

属性类型描述
.style.widthASDimension指定 ASLayoutElement 内容区域的宽度。minWidth 和 maxWidth 属性会覆盖 width,默认值为 ASDimensionAuto
.style.heightASDimension指定ASLayoutElement 内容区域的高度。minHeight 和 maxHeight 属性会覆盖 height,默认值为 ASDimensionAuto
.style.minWidthASDimensionminWidth 属性用于设置一个特定布局元素的最小宽度.它可以防止 width 属性值小于 minWidth 指定的值,minWidth 的值会覆盖 maxWidth 和 width. 默认值为 ASDimensionAuto
.style.maxWidthASDimensionmaxWidth 属性用于设置一个特定布局元素的最大宽度.它可以防止 width 属性值大于 maxWidth 指定的值. maxWidth 的值会覆盖 width,minWidth 会覆盖 maxWidth. 默认值为 ASDimensionAuto
.style.minHeightASDimensionminHeight 属性用于设置一个特定布局元素的最小高度.它可以防止 height 属性值小于 minHeight 指定的值.minHeight 的值会覆盖 maxHeight 和 height. 默认值为 ASDimensionAuto
.style.maxHeightASDimensionmaxHeight 属性用于设置一个特定布局元素的最大高度,它可以防止 height 属性值大于 maxHeight 指定的值.maxHeight 的值会覆盖 height,minHeight 会覆盖 maxHeight.默认值为 ASDimensionAuto
.style.preferredSizeCGSize提供布局元素的建议 size.如果提供了 minSize 或 maxSize,并且 preferredSize 超过了这些值,则强制使用 minSize 或 maxSize.如果未提供 preferredSize,则布局元素的 size 默认为 calculateSizeThatFits: 方法提供的固有大小.此方法是可选的,但是对于没有固有大小或需要用与固有大小不同的的 size 进行布局的节点,则必须指定 preferredSize 或 preferredLayoutSize 中的一个,比如没这个属性可以在 ASImageNode 上设置,使这个节点的 size 和图片 size 不同, 警告:当 size 的宽度或高度是相对值时调用 getter 将进行断言
.style.minSizeCGSize可选属性,为布局元素提供最小尺寸,如果提供,minSize 将会强制使用.如果父级布局元素的 minSize 小于其子级的 minSize,则强制使用子级的 minSize,并且其大小将扩展到布局规则之外,例如,如果给全屏容器中的某个元素设置 50% 的 preferredSize 相对宽度,和 200pt 的 minSize 宽度,preferredSize 会在 iPhone 屏幕上产生 160pt 的宽度,但由于 160pt 低于 200pt 的 minSize 宽度,因此最终该元素的宽度会是 200pt
.style.maxSizeCGSize可选属性,为布局元素提供最大尺寸,如果提供,maxSize 将会强制使用
.style.preferredLayoutSizeASLayoutSize为布局元素提供建议的相对 size.ASLayoutSize 使用百分比而不是点来指定布局.例如,子布局元素的宽度应该是父宽度的 50%.如果提供了可选的 minLayoutSize 或 maxLayoutSize,并且 preferredLayoutSize 超过了这些值,则将使用 minLayoutSize 或 maxLayoutSize
.style.minLayoutSizeASLayoutSize可选属性,为布局元素提供最小的相对尺寸, 如果提供,minLayoutSize 将会强制使用.如果父级布局元素的 minLayoutSize 小于其子级的 minLayoutSize,则会强制使用子级的 minLayoutSize,并且其大小将扩展到布局规则之外
.style.maxLayoutSizeASLayoutSize可选属性,为布局元素提供最大的相对尺寸.如果提供,maxLayoutSize 将会强制使用.如果父级布局元素的 maxLayoutSize 小于其子级的 maxLayoutSize,那么将强制使用子级的 maxLayoutSize,并且其大小将扩展到布局规则之外

Layout API Sizing

在Layout API中,理解复合尺寸类型的最简单方法是查看所有的 unit 之间的关系。

texture-sizing

Values (CGFloat, ASDimension)

ASDimension本质上是一个普通的CGFloat,支持表示点值,相对百分比值或自动值。

unit 允许相同的API接受固定值和相对值。

1
2
3
4
5
6
7
8
// dimension returned is relative (%)
ASDimensionMake("50%")
ASDimensionMakeWithFraction(0.5)

// dimension returned in points
ASDimensionMake("70pt")
ASDimensionMake(70)
ASDimensionMakeWithPoints(70)

使用 ASDimension 的例子

ASDimension 用于设置 ASStackLayoutSpec 中element 的 flexBasis. flexBasis 属性在 stack layout 中指定对象的初始大小,

在下面的视图中,我们希望左侧堆栈占据水平宽度的40%,右侧堆栈占据宽度的60%。

flexbasis

为此,我们在水平堆栈的两个子级上设置.flexBasis属性:

1
2
3
4
self.leftStack.style.flexBasis = ASDimensionMake("40%")
self.rightStack.style.flexBasis = ASDimensionMake("60%")

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

Sizes (CGSize, ASLayoutSize)

ASLayoutSize与CGSize相似,但是其宽度和高度值可以表示点或百分比值。宽度和高度的类型是独立的;一个可以是点或百分比值。

1
ASLayoutSizeMake(ASDimension width, ASDimension height);

ASLayoutSize用于设置布局元素的 .preferredLayoutSize , .minLayoutSize.maxLayoutSize 属性。它允许相同的API接受固定大小以及相对大小。

1
2
3
4
5
// Dimension type "Auto" indicates that the layout element may 
// be resolved in whatever way makes most sense given the circumstances
let width = ASDimensionMake(.auto, 0)
let height = ASDimensionMake("50%")
layoutElement.style.preferredLayoutSize = ASLayoutSizeMake(width, height)

如果不需要相对值,则可以设置布局元素的.preferredSize,.minSize和.maxSize属性。这些属性采用常规CGSize值。

1
layoutElement.style.preferredSize = CGSize(width: 30, height: 60)

大多数时候,都不想同时限制宽度和高度。在这种情况下,可以使用ASDimension值分别设置布局元素的尺寸属性。

1
2
3
4
5
6
7
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%")

Size Range (ASSizeRange)

UIKit没有提供组合最小和最大CGSize的 struct。因此,创建了ASSizeRange以支持最小和最大CGSize对。

ASSizeRange通常用于布局API的内部。但是,作为输入传递给layoutSpecThatFits:的constrainedSize值是ASSizeRange。

1
func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec

传递给ASDisplayNode子类的layoutSpecThatFits:方法的constrainedSize是节点应适合的最小和最大大小。constrainedSize中包含的最小CGSize和最大CGSize可用于调整节点的布局元素的大小。