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
在初始化以后会检查是否有子视图,若是有就会调用objective-c
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
方法进行布局,因此对视图进行布局须要重写这个方法。看一个例子:swift
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_childNode]; return insetLayout; }
_childNode
相对于父视图边距都为 0,也就是AutoLayout
中 top
bottom
left
right
都为 0。数组
-----------------------------父视图---------------------------- | -------------------------_childNode--------------------- | | | | | | | | | | --------------------------- --------------------------- | --------------------------------------------------------------
能够看到layoutSpecThatFits:
方法返回的必须是 ASLayoutSpec
, ASInsetLayoutSpec
是它的子类之一,下面是全部的子类及其关系:网络
ASLayoutSpecapp
ASRelativeLayoutSpec // 顶点布局less
_ide
使用方法和原生的绝对布局相似布局
- (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
也就失去它本来的布局的意义。
ASOverlayLayoutSpec *contentLayout = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:stackLayout overlay:absoluteLayout];
不过官方文档明确指出应该尽可能少用这种布局方式:
Absolute layouts are less flexible and harder to maintain than other types of layouts.
_
- (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 horizontalStackLayoutSpec]; 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)
代替,并不须要给出具体数值。