Texture
擁有本身的一套成熟佈局方案,雖然學習成本略高,但至少比原生的 AutoLayout
寫起來舒服,重點是性能遠好於 AutoLayout
,Texture
文檔上也指出了這套佈局方案的的優勢:html
首先這套佈局都是基於 Texture
組件的,因此當遇到要使用原生控件時,經過用 block 的方式包裝一個原生組件再合適不過了,例如:node
ASDisplayNode *animationImageNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{
FLAnimatedImageView *animationImageView = [[FLAnimatedImageView alloc] init];
animationImageView.layer.cornerRadius = 2.0f;
animationImageView.clipsToBounds = YES;
return animationImageView;
}];
[self addSubnode:animationImageNode];
self.animationImageNode = animationImageNode;
複製代碼
ASDisplayNode
在初始化以後會檢查是否有子視圖,若是有就會調用git
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
複製代碼
方法進行佈局,因此對視圖進行佈局須要重寫這個方法。看一個例子:github
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_childNode];
return insetLayout;
}
複製代碼
_childNode
相對於父視圖邊距都爲 0,也就是AutoLayout
中 top
bottom
left
right
都爲 0。objective-c
-----------------------------父視圖----------------------------
| -------------------------_childNode--------------------- |
| | | |
| | | |
| --------------------------- --------------------------- |
--------------------------------------------------------------
複製代碼
能夠看到layoutSpecThatFits:
方法返回的必須是 ASLayoutSpec
, ASInsetLayoutSpec
是它的子類之一,下面是全部的子類及其關係:swift
使用方法和原生的絕對佈局相似數組
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
self.childNode.style.layoutPosition = CGPointMake(100, 100);
self.childNode.style.preferredLayoutSize = ASLayoutSizeMake(ASDimensionMake(100), ASDimensionMake(100));
ASAbsoluteLayoutSpec *absoluteLayout = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[self.childNode]];
return absoluteLayout;
}
複製代碼
值得提的是:ASAbsoluteLayoutSpec 通常狀況都會經過 ASOverlayLayoutSpec
或 ASOverlayLayoutSpec
着陸,由於只有上述兩種佈局才能保留 ASAbsoluteLayoutSpec 絕對佈局的事實。舉個例子當視圖中只有一個控件須要用的是 ASAbsoluteLayoutSpec
佈局,而其餘控件佈局用的是 ASStackLayoutSpec
(後面會介紹),那麼一旦 absoluteLayout 被加入到 ASStackLayoutSpec
也就失去它本來的佈局的意義。bash
ASOverlayLayoutSpec *contentLayout = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:stackLayout overlay:absoluteLayout];
複製代碼
不過官方文檔明確指出應該儘可能少用這種佈局方式:網絡
Absolute layouts are less flexible and harder to maintain than other types of layouts.app
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
ASBackgroundLayoutSpec *backgroundLayout = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:self.childNodeB background:self.childNodeA];
return backgroundLayout;
}
複製代碼
把childNodeA
作爲 childNodeB
的背景,也就是 childNodeB
在上層,**要注意的是 **ASBackgroundLayoutSpec
事實上根本不會改變視圖的層級關係,好比:
ASDisplayNode *childNodeB = [[ASDisplayNode alloc] init];
childNodeB.backgroundColor = [UIColor blueColor];
[self addSubnode:childNodeB];
self.childNodeB = childNodeB;
ASDisplayNode *childNodeA = [[ASDisplayNode alloc] init];
childNodeA.backgroundColor = [UIColor redColor];
[self addSubnode:childNodeA];
self.childNodeA = childNodeA;
複製代碼
那麼即便使用上面的佈局方式,childNodeB
依然在下層。
比較經常使用的一個類,看圖應該能一目瞭然(圖片來自於官方文檔)
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_childNode];
return insetLayout;
}
複製代碼
_childNode
相對於父視圖邊距都爲 0,至關於填充整個父視圖。它和以後會說到的 ASOverlayLayoutSpec
實際上更多的用來組合兩個 Element
而已。
參考 ASBackgroundLayoutSpec
也是比較經常使用的一個類,做用是設置自身的高寬比,例如設置正方形的視圖
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
ASRatioLayoutSpec *ratioLayout = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.0f child:self.childNodeA];
return ratioLayout;
}
複製代碼
把它稱爲頂點佈局可能有點不恰當,實際上它能夠把視圖佈局在:左上
、左下
、右上
、右下
四個頂點之外,還能夠設置成居中佈局。
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
self.childNodeA.style.preferredSize = CGSizeMake(100, 100);
ASRelativeLayoutSpec *relativeLayout = [ASRelativeLayoutSpec relativePositionLayoutSpecWithHorizontalPosition:ASRelativeLayoutSpecPositionEnd verticalPosition:ASRelativeLayoutSpecPositionStart sizingOption:ASRelativeLayoutSpecSizingOptionDefault child:self.childNodeA];
return relativeLayout;
}
複製代碼
上面的例子就是把 childNodeA
顯示在右上角。
絕大多數狀況下用來居中顯示視圖
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
self.childNodeA.style.preferredSize = CGSizeMake(100, 100);
ASCenterLayoutSpec *relativeLayout = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:self.childNodeA];
return relativeLayout;
}
複製代碼
能夠說這是最經常使用的類,並且相對於其餘類來講在功能上是最接近於 AutoLayout
的。 之因此稱之爲盒子佈局是由於它和 CSS 中 Flexbox
很類似,關於 Flexbox
的能夠看下阮一峯的這篇文章。
先看一個例子:
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
self.childNodeA.style.preferredSize = CGSizeMake(100, 100);
self.childNodeB.style.preferredSize = CGSizeMake(200, 200);
ASStackLayoutSpec *stackLayout = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
spacing:12
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsStart
children:@[self.childNodeA, self.childNodeB]];
return stackLayout;
}
複製代碼
簡單的說明下各個參數的做用:
direction
:主軸的方向,有兩個可選值:ASStackLayoutDirectionVertical
ASStackLayoutDirectionHorizontal
spacing
: 主軸上視圖排列的間距,好比有四個視圖,那麼它們之間的存在三個間距值都應該是spacing
justifyContent
: 主軸上的排列方式,有五個可選值:ASStackLayoutJustifyContentStart
從前日後排列ASStackLayoutJustifyContentCenter
居中排列
ASStackLayoutJustifyContentEnd
從後往前排列ASStackLayoutJustifyContentSpaceBetween
間隔排列,兩端無間隔ASStackLayoutJustifyContentSpaceAround
間隔排列,兩端有間隔alignItems
: 交叉軸上的排列方式,有五個可選值:ASStackLayoutAlignItemsStart
從前日後排列ASStackLayoutAlignItemsEnd
從後往前排列
ASStackLayoutAlignItemsCenter
居中排列ASStackLayoutAlignItemsStretch
拉伸排列ASStackLayoutAlignItemsBaselineFirst
以第一個文字元素基線排列(主軸是橫向纔可用)ASStackLayoutAlignItemsBaselineLast
以最後一個文字元素基線排列(主軸是橫向纔可用)children
: 包含的視圖。數組內元素順序一樣表明着佈局時排列的順序,因此須要注意主軸的方向設置尤其重要,若是主軸設置的是 ASStackLayoutDirectionVertical
, 那麼 justifyContent
各個參數的意義就是:
ASStackLayoutJustifyContentStart
從上往下排列ASStackLayoutJustifyContentCenter
居中排列ASStackLayoutJustifyContentEnd
從下往上排列ASStackLayoutJustifyContentSpaceBetween
間隔排列,兩端無間隔ASStackLayoutJustifyContentSpaceAround
間隔排列,兩端有間隔alignItems
就是:
ASStackLayoutAlignItemsStart
從左往右排列ASStackLayoutAlignItemsEnd
從右往左排列ASStackLayoutAlignItemsCenter
居中排列ASStackLayoutAlignItemsStretch
拉伸排列ASStackLayoutAlignItemsBaselineFirst
無效ASStackLayoutAlignItemsBaselineLast
無效對於子視圖間距不同的佈局方法,後面實戰中會講到。
填充整個視圖
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
ASWrapperLayoutSpec *wrapperLayout = [ASWrapperLayoutSpec wrapperWithLayoutElement:self.childNodeA];
return wrapperLayout;
}
複製代碼
顧名思義 ASCornerLayoutSpec 適用於相似於角標的佈局
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
let cornerSpec = ASCornerLayoutSpec(child: avatarNode, corner: badgeNode, location: .topRight)
cornerSpec.offset = CGPoint(x: -3, y: 3)
}
複製代碼
最須要注意的是offset
是控件的Center的偏移
簡單的文件覆蓋在圖片上,文字居中。
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
ASWrapperLayoutSpec *wrapperLayout = [ASWrapperLayoutSpec wrapperWithLayoutElement:self.coverImageNode];
ASCenterLayoutSpec *centerSpec = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:self.textNode];
ASOverlayLayoutSpec *overSpec = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:wrapperLayout overlay:centerSpec];
return overSpec;
}
複製代碼
ASWrapperLayoutSpec
把圖片鋪滿整個視圖ASCenterLayoutSpec
把文字居中顯示ASOverlayLayoutSpec
把文字覆蓋到圖片上注意第三步就是以前提到的 ASOverlayLayoutSpec
/ASBackgroundLayoutSpec
的做用:用於組合兩個 Element
。
這個是輕芒閱讀(豌豆莢一覽) APP 內 AppSo 頻道 Cell 的佈局,應該也是比較典型的佈局之一。爲了方便理解先給各個元素定一下名稱,從上至下,從左往右分別是:
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
self.shareImageNode.style.preferredSize = CGSizeMake(15, 15);
self.likeImageNode.style.preferredSize = CGSizeMake(15, 15);
ASStackLayoutSpec *likeLayout = [ASStackLayoutSpec horizontalStackLayoutSpec];
likeLayout.spacing = 4.0;
likeLayout.justifyContent = ASStackLayoutJustifyContentStart;
likeLayout.alignItems = ASStackLayoutAlignItemsCenter;
likeLayout.children = @[self.likeImageNode, self.likeNumberNode];
ASStackLayoutSpec *shareLayout = [ASStackLayoutSpec horizontalStackLayoutSpec];
shareLayout.spacing = 4.0;
shareLayout.justifyContent = ASStackLayoutJustifyContentStart;
shareLayout.alignItems = ASStackLayoutAlignItemsCenter;
shareLayout.children = @[self.shareImageNode, self.shareNumberNode];
ASStackLayoutSpec *otherLayout = [ASStackLayoutSpec horizontalStackLayoutSpec];
otherLayout.spacing = 12.0;
otherLayout.justifyContent = ASStackLayoutJustifyContentStart;
otherLayout.alignItems = ASStackLayoutAlignItemsCenter;
otherLayout.children = @[likeLayout, shareLayout];
ASStackLayoutSpec *bottomLayout = [ASStackLayoutSpec horizontalStackLayoutSpec];
bottomLayout.justifyContent = ASStackLayoutJustifyContentSpaceBetween;
bottomLayout.alignItems = ASStackLayoutAlignItemsCenter;
bottomLayout.children = @[self.dateTextNode, otherLayout];
self.titleNode.style.spacingBefore = 12.0f;
self.subTitleNode.style.spacingBefore = 16.0f;
self.subTitleNode.style.spacingAfter = 20.0f;
ASRatioLayoutSpec *rationLayout = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:0.5 child:self.coverImageNode];
ASStackLayoutSpec *contentLayout = [ASStackLayoutSpec verticalStackLayoutSpec];
contentLayout.justifyContent = ASStackLayoutJustifyContentStart;
contentLayout.alignItems = ASStackLayoutAlignItemsStretch;
contentLayout.children = @[
rationLayout,
self.titleNode,
self.subTitleNode,
bottomLayout
];
ASInsetLayoutSpec *insetLayout = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(16, 16, 16, 16) child:contentLayout];
return insetLayout;
}
複製代碼
下面詳細解釋下佈局,不過首先要明確的是,Texture 的這套佈局方式遵照從裏到外的佈局原則,使用起來纔會駕輕就熟。
ASStackLayoutSpec
佈局 分享圖標
和 分享數量
、 喜歡圖標
和 喜歡數量
。ASStackLayoutSpec
包裝第一步的兩個的佈局獲得 otherLayout
佈局對象。ASStackLayoutSpec
包裝otherLayout
和 發佈時間
。注意這裏設置橫向的排列方式 ASStackLayoutJustifyContentSpaceBetween
已到達兩端佈局的目的,最終返回 bottomLayout
。大圖
是網絡圖片,對於 Cell 來講,子視圖的佈局必能能決定其高度(Cell 寬度是默認等於 TableNode 的寬度),因此這裏必須設置 大圖
的高度,ASRatioLayoutSpec
設置了圖片的高寬比。大圖
、標題
、副標題
、bottomLayout
的一個縱向佈局,能夠發現這裏的視圖間距並不相同,這時候 spacingBefore
和 spacingAfter
就會頗有用,它們用來分別設置元素在主軸上的先後間距。self.titleNode.style.spacingBefore = 12.0f;
意思就是 標題
相對於 大圖
間距爲 12。ASInsetLayoutSpec
設置一個邊距。能夠看到不只是 Node
,ASLayoutSpec
自己也能夠做爲佈局元素,這是由於只要是遵照了 <ASLayoutElement>
協議的對象均可以做爲佈局元素。
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
self.node1.style.preferredSize = CGSize(width: constrainedSize.max.width, height: 136)
self.node2.style.preferredSize = CGSize(width: 58, height: 25)
self.node2.style.layoutPosition = CGPoint(x: 14.0, y: 95.0)
self.node3.style.height = ASDimensionMake(37.0)
self.node4.style.preferredSize = CGSize(width: 80, height: 20)
self.node5.style.preferredSize = CGSize(width: 80, height: 20)
self.node4.style.spacingBefore = 14.0
self.node5.style.spacingAfter = 14.0
let absoluteLayout = ASAbsoluteLayoutSpec(children: [self.node2])
let overlyLayout = ASOverlayLayoutSpec(child: self.node1, overlay: absoluteLayout)
let insetLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(0, 14, 0, 14), child: self.node3)
insetLayout.style.spacingBefore = 13.0
insetLayout.style.spacingAfter = 25.0
let bottomLayout = ASStackLayoutSpec.horizontal()
bottomLayout.justifyContent = .spaceBetween
bottomLayout.alignItems = .start
bottomLayout.children = [self.node4, self.node5]
bottomLayout.style.spacingAfter = 10.0
// bottomLayout.style.width = ASDimensionMake(constrainedSize.max.width)
let stackLayout = ASStackLayoutSpec.vertical()
stackLayout.justifyContent = .start
stackLayout.alignItems = .stretch
stackLayout.children = [overlyLayout, insetLayout, bottomLayout]
return stackLayout
}
複製代碼
爲了演示 ASAbsoluteLayoutSpec 的使用,這裏 node3 咱們用 ASAbsoluteLayoutSpec 佈局。
接下來講下要點:
width
(如註釋)或它的上一級佈局對象的 alignItems,在例子中就是 stackLayout.alignItems = .stretch
此案例主要爲了演示 flexGrow
的用法,先介紹下 flexGrow 的做用(來自於簡書九彩拼盤)
該屬性來設置,當父元素的寬度大於全部子元素的寬度的和時(即父元素會有剩餘空間),子元素如何分配父元素的剩餘空間。
flex-grow的默認值爲0,意思是該元素不索取父元素的剩餘空間,若是值大於0,表示索取。值越大,索取的越厲害。舉個例子:
父元素寬400px,有兩子元素:A和B。A寬爲100px,B寬爲200px,則空餘空間爲 400-(100+200)= 100px。
若是A,B都不索取剩餘空間,則有100px的空餘空間。
若是A索取剩餘空間:設置flex-grow爲1,B不索取。則最終A的大小爲 自身寬度(100px)+ 剩餘空間的寬度(100px)= 200px
若是A,B都設索取剩餘空間,A設置flex-grow爲1,B設置flex-grow爲2。則最終A的大小爲 自身寬度(100px)+ A得到的剩餘空間的寬度(100px * (1/(1+2))),最終B的大小爲 自身寬度(200px)+ B得到的剩餘空間的寬度(100px * (2/(1+2)))
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
self.node1.style.height = ASDimensionMake(20.0)
var imageLayoutArray = [ASLayoutElement]()
[self.node2, self.node3, self.node4].forEach { (node) in
let layout = ASRatioLayoutSpec(ratio: 2.0/3.0, child: node)
layout.style.flexGrow = 1 // 至關於寬度相等
imageLayoutArray.append(layout)
}
let imageLayout = ASStackLayoutSpec.horizontal()
imageLayout.justifyContent = .start
imageLayout.alignItems = .start
imageLayout.spacing = 14.0
imageLayout.children = imageLayoutArray
let contentLayout = ASStackLayoutSpec.vertical()
contentLayout.justifyContent = .start
contentLayout.alignItems = .stretch
contentLayout.spacing = 22.0
contentLayout.children = [self.node1, imageLayout]
return ASInsetLayoutSpec(insets: UIEdgeInsetsMake(22.0, 16.0, 22.0, 16.0), child: contentLayout)
}
複製代碼
在這個案例中 node二、node三、node4 的寬度的總和小於父元素的寬度,因此爲了達到寬度相同只須要設置三者的 flexGrow 相同就行(都爲1),再經過 ASRatioLayoutSpec 固定各自的寬高比,那麼對於這個三個控件來講最終的寬度是肯定的。
此案例主要爲了演示 flexShrink
的用法,一樣還來自於簡書九彩拼盤關於 flexShrink 的介紹
該屬性來設置,當父元素的寬度小於全部子元素的寬度的和時(即子元素會超出父元素),子元素如何縮小本身的寬度的。
flex-shrink的默認值爲1,當父元素的寬度小於全部子元素的寬度的和時,子元素的寬度會減少。值越大,減少的越厲害。若是值爲0,表示不減少。
舉個例子:父元素寬400px,有兩子元素:A和B。A寬爲200px,B寬爲300px。則A,B總共超出父元素的寬度爲(200+300)- 400 = 100px。
若是A,B都不減少寬度,即都設置flex-shrink爲0,則會有100px的寬度超出父元素。若是A不減少寬度:設置flex-shrink爲0,B減少。則最終B的大小爲 自身寬度(300px)- 總共超出父元素的寬度(100px)= 200px若是A,B都減少寬度,A設置flex-shirk爲3,B設置flex-shirk爲2。則最終A的大小爲 自身寬度(200px)- A減少的寬度(100px * (200px * 3/(200 * 3 + 300 * 2))) = 150px,最終B的大小爲 自身寬度(300px)- B減少的寬度(100px * (300px * 2/(200 * 3 + 300 * 2))) = 250px
目前關於該屬性最多見仍是用於對文本的寬度限制,在上圖中 textNode 和 displayNode 是兩端對齊,並且須要限制文本的最大寬度,這時候設置 flexShrink
是最方便的。
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
self.displayNode.style.preferredSize = CGSize(width: 42.0, height: 18.0)
self.textNode.style.flexShrink = 1
let contentLayout = ASStackLayoutSpec.horizontal()
contentLayout.justifyContent = .spaceBetween
contentLayout.alignItems = .start
contentLayout.children = [self.textNode, self.displayNode]
let insetLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(16.0, 16.0, 16.0, 16.0), child: contentLayout)
return insetLayout
}
複製代碼
隨便提一下的是若是 ASTextNode 出現莫名的文本截斷問題,能夠用 ASTextNode2 代替。
還算比較典型的例子
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let otherLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(10.0, 10.0, CGFloat(Float.infinity), CGFloat(Float.infinity)), child: topLeftNode)
let contentLayout = ASOverlayLayoutSpec(child: coverImageNode, overlay: otherLayout)
return contentLayout
}
複製代碼
利用 ASInsetLayoutSpec 是最好的解決方案,值得注意的是對於紅色控件只須要設置向上和向左的間距,那麼其餘方向的能夠用 CGFloat(Float.infinity)
代替,並不須要給出具體數值。
最後,以上的案例已上傳至在TextureLayoutDem。