[譯] AsyncDisplayKit/Texture 官方文件(2)

薛定諤發表於2019-03-01

官方文件連結: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 的示例

未完待續

相關文章