佈局快速入門
[譯] AsyncDisplayKit/Texture 官方文件(1)
開發初衷和優勢
Layout API 的出現是為了提供一種可以替代 UIKit Auto Layout 的高效能方案,UIKit Auto Layout 在複雜的檢視結構中,計算量會呈指數級增長,Texture 的佈局方案相對 Auto Layout 有以下優點:
- 快:Texture 的佈局計算和手寫 Frame 一樣的快;
- 非同步和併發:佈局可以在後臺執行緒上計算,使用者互動不會因此而中斷;
- 宣告式渲染:佈局使用不可變的資料結構宣告,這讓佈局程式碼變得更容易開發、維護、除錯、測試和評審;
- 可快取:如果佈局是不可變的,可以在後臺預先計算並快取,這可以讓使用者感覺更快;
- 可擴充套件:在不同的類中使用相同的佈局會變得很方便;
靈感來自於 CSS Flexbox
熟悉 Flexbox 的人會注意到這兩個系統有許多的相似之處, 但 Layout API 並沒有重新實現所有的 CSS。
基本概念
Texture 的佈局主要圍繞兩個概念展開:
- 佈局規則
- 佈局元素
佈局規則/Layout Specs
佈局規則沒有物理存在,它通過充當 LayoutElements
的容器,理解多個 LayoutElements
之間的關聯,完成 LayoutElements
的位置排列。Texture 提供了 ASLayoutSpec
的幾個子類,涵蓋了從插入單個佈局元素的簡單規則,到可以變化堆放排列配置,包含多個佈局元素的複雜規則。
佈局元素/Layout Elements
LayoutSpecs
包含 LayoutElements
,並對 LayoutElements
進行整理。
所有的 ASDisplayNode
和 ASLayoutSpec
都遵守 <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
在任何 ASDisplayNode
或 ASLayoutSpec
上呼叫 -asciiArtString
都會返回該物件及其子項的字元圖。 你也可以在任何 Node
或 layoutSpec
中設定 .debugName
,這樣也將包含字元圖,下面是一個示例:
-----------------------ASStackLayoutSpec----------------------
| -----ASStackLayoutSpec----- -----ASStackLayoutSpec----- |
| | ASImageNode | | ASImageNode | |
| | ASImageNode | | ASImageNode | |
| --------------------------- --------------------------- |
--------------------------------------------------------------複製程式碼
你還可以在任何 ASLayoutElement
,比如 Node
和 layoutSpec
上列印樣式物件,這在除錯 .size
屬性時特別有用。
(lldb) po _photoImageNode.style
Layout Size = min {414pt, 414pt} <= preferred {20%, 50%} <= max {414pt, 414pt}複製程式碼
佈局示例
點選檢視layoutSpec
的示例工程。
簡單的文字左對齊和右對齊
為了建立這個一個佈局,我們將使用:
- 表示垂直的
ASStackLayoutSpec
; - 表示水平的
ASStackLayoutSpec
; - 插入標題的
ASInsetLayoutSpec
;
下圖展示了一個由 Node
和 LayoutSpecs
組成的佈局元素:
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)
}複製程式碼
圖片上覆蓋圖示
要建立這個佈局,我們將用到:
- 設定
size
和position
的ASLayoutable
屬性;
- 用於放置圖片和圖示的
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
是如何通過 layoutSpecs
和 Node
組成的:
以下的程式碼也可以在 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
來確定其子元素的 size
和 position
。 Flexbox 旨在為不同的螢幕尺寸提供一致的佈局, 在盒子佈局中,你垂直或水平的對其元素。 盒子佈局也可以是另一個盒子的子佈局,這使得盒子佈局規則幾乎可以勝任任何的佈局。
除了 ASLayoutElement
屬性,ASStackLayoutSpec
還有 7 個屬性:
-
direction
指定子元素的排序方向,如果設定了
horizontalAlignment
和verticalAlignment
,它們將被再次解析,這會導致justifyContent
和alignItems
也會相應地更新。 -
spacing
描述子元素之間的距離
-
horizontalAlignment
指定子元素如何在水平方向上對齊,它的實際效果取決於
direction
,設定對齊會使justifyContent
或alignItems
更新。在direction
改變之後,對齊方式仍然有效,因此,這是一個優先順序高的屬性。 -
verticalAlignment
指定子元素如何在垂直方向上對齊,它的實際效果取決於
direction
,設定對齊會使justifyContent
或alignItems
更新。在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
傳遞給它的子節點, 一旦子節點確定了它的 size
,insetSpec
將它的最終 size
作為子節點的 size
和 margin
。
由於 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
將其上面的子節點(紅色)延伸,覆蓋一個子節點(藍色)。
overlaySpec
的 size
根據子節點的 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
設定一個子節點(藍色)為內容,將背後的另一個子節點拉伸為背景(紅色)。
ASBackgroundLayoutSpec
的 size
根據子節點的 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
,因為它使用這個值來進行計算。
使用 ASRatioLayoutSpec
為 ASNetworkImageNode
或 ASVideoNode
提供固有大小是非常常見的,因為兩者在內容從伺服器返回之前都沒有固有大小。
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:只會在盒子佈局中的的
subnode
或layoutSpec
中生效; - ASAbsoluteLayoutElement Properties:只會在絕對佈局中的的
subnode
或layoutSpec
中生效; - ASLayoutElement Properties:適用於所有
Node
和layoutSpec
;
ASStackLayoutElement Properties
請注意,以下屬性只有在 ASStackLayout
的 subnode
上設定才會生效。
.style.spacingBefore
CGFloat
型別,direction 上與前一個 node 的間隔。
.style.spacingAfter
CGFloat
型別,direction 上與後一個 node 的間隔。
.style.flexGrow
Bool
型別,子節點尺寸總和小於 minimum ,即存在剩餘空間時,是否放大。
.style.flexShrink
Bool
型別,子節點總和大於 maximum,即空間不足時,是否縮小。
.style.flexBasis
ASDimension
型別,描述在剩餘空間是均分的情況下,應用 flexGrow
或 flexShrink
屬性之前,該物件在盒子中垂直或水平方向的初始 size
,
.style.alignSelf
ASStackLayoutAlignSelf
型別,描述物件在十字軸的方向,此屬性會覆蓋 alignItems
,可選值有:
ASStackLayoutAlignSelfAuto
ASStackLayoutAlignSelfStart
ASStackLayoutAlignSelfEnd
ASStackLayoutAlignSelfCenter
ASStackLayoutAlignSelfStretch
.style.ascender
CGFloat
型別,用於基線對齊,描述物件從頂部到其基線的距離。
.style.descender
CGFloat
型別,用於基線對齊,描述物件從基線到其底部的距離。
ASAbsoluteLayoutElement Properties
請注意,以下屬性只有在 AbsoluteLayout
的 subnode
上設定才會生效。
.style.layoutPosition
CGPoint
型別,描述該物件在 ASAbsoluteLayoutSpec
父規則中的位置。
ASLayoutElement Properties
請注意,以下屬性適用於所有佈局元素。
.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 的各種型別最簡單方法是檢視所有單位之間的相互關係。
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
,我們建立了 ASSizeRange
, ASSizeRange
主要應用在 Llayout API 的內部,但是 layoutSpecThatFits:
方法的的輸入引數 constrainedSize
是 ASSizeRange
型別。
func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec複製程式碼
傳遞給 ASDisplayNode
子類 layoutSpecThatFits:
方法的 constrainedSize
是 Node
最適合的最小和最大尺寸,你可以在佈局元素上使用 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)
}複製程式碼
為了在本例中觸發從 nameField
到 ageField
的轉換,我們將更新 SignupNode
的 .fieldState
屬性,並使用 transitionLayoutWithAnimation
方法觸發動畫。
這個方法將使當前計算的佈局失效,並重新計算 ageField
在盒子中的佈局。
self.signupNode.fieldState = .signupNodeName
self.signupNode.transitionLayout(withAnimation: true, shouldMeasureAsync: true)複製程式碼
在這個 API 的預設實現中,佈局將重新計算,並使用它的 sublayouts
來對 SignupNode
子節點的 size
和 position
進行設定,但沒有動畫。這個 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
發生變化時非常有用。