[聚合文章] Texture 布局篇

CSS 2017-12-15 22 阅读

Texture 拥有自己的一套成熟布局方案,虽然学习成本略高,但至少比原生的 AutoLayout 写起来舒服,重点是性能远好于 AutoLayoutTexture 文档上也指出了这套布局方案的的优点:

  • Fast : As fast as manual layout code and significantly faster than Auto Layout
  • Asynchronous & Concurrent : Layouts can be computed on background threads so user interactions are not interrupted.
  • Declarative : Layouts are declared with immutable data structures. This makes layout code easier to develop, document, code review, test, debug, profile, and maintain.
  • Cacheable : Layout results are immutable data structures so they can be precomputed in the background and cached to increase user perceived performance.
  • Extensible : Easy to share code between classes.

首先这套布局都是基于 Texture 组件的,所以当遇到要使用原生控件时,通过用 block 的方式包装一个原生组件再合适不过了,例如:

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 在初始化之后会检查是否有子视图,如果有就会调用

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize

方法进行布局,所以对视图进行布局需要重写这个方法。看一个例子:

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_childNode];
    return insetLayout;
}

_childNode 相对于父视图边距都为 0,也就是 AutoLayouttop bottom left right 都为 0。

-----------------------------父视图----------------------------
|  -------------------------_childNode---------------------  |
|  |                                                      |  |
|  |                                                      |  |
|  ---------------------------  ---------------------------  |
--------------------------------------------------------------

可以看到 layoutSpecThatFits: 方法返回的必须是 ASLayoutSpec , ASInsetLayoutSpec 是它的子类之一,下面是所有的子类及其关系:

  • ASLayoutSpec
    • ASAbsoluteLayoutSpec // 绝对布局
    • ASBackgroundLayoutSpec // 背景布局
    • ASInsetLayoutSpec // 边距布局
    • ASOverlayLayoutSpec // 覆盖布局
    • ASRatioLayoutSpec // 比例布局
    • ASRelativeLayoutSpec // 顶点布局
      • ASCenterLayoutSpec // 居中布局
    • ASStackLayoutSpec // 盒子布局
    • ASWrapperLayoutSpec // 填充布局

ASAbsoluteLayoutSpec

使用方法和原生的绝对布局类似

- (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 一般情况都会通过 ASOverlayLayoutSpecASOverlayLayoutSpec 着陆。举个例子当视图中只有一个控件需要用的是 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.

ASBackgroundLayoutSpec

- (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 依然在下层。它和之后会说到的 ASOverlayLayoutSpec 实际上更多的用来组合两个 Element 而已。

ASInsetLayoutSpec

比较常用的一个类,看图应该能一目了然(图片来自于 官方文档

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_childNode];
    return insetLayout;
}

_childNode 相对于父视图边距都为 0,相当于填充整个父视图。

ASOverlayLayoutSpec

参考 ASBackgroundLayoutSpec

ASRatioLayoutSpec

(图片来自于 官方文档

也是比较常用的一个类,作用是设置自身的高宽比,例如设置正方形的视图

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    ASRatioLayoutSpec *ratioLayout = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.0f child:self.childNodeA];
    return ratioLayout;
}

ASRelativeLayoutSpec

把它称为 顶点布局 可能有点不恰当,实际上它可以把视图布局在: 左上左下右上右下 四个顶点以外,还可以设置成居中布局。

- (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 显示在右上角。

ASCenterLayoutSpec

绝大多数情况下用来居中显示视图

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    self.childNodeA.style.preferredSize = CGSizeMake(100, 100);
    ASCenterLayoutSpec *relativeLayout = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:self.childNodeA];
    return relativeLayout;
}

ASStackLayoutSpec

可以说这是 最常用的类 ,而且相对于其他类来说在功能上是最接近于 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;
}

简单的说明下各个参数的作用:

  1. direction :主轴的方向,有两个可选值:
    • 纵向: ASStackLayoutDirectionVertical
    • 横向: ASStackLayoutDirectionHorizontal
  2. spacing : 主轴上视图排列的间距,比如有四个视图,那么它们之间的存在三个间距值都应该是 spacing
  3. justifyContent : 主轴上的排列方式,有五个可选值:
    • ASStackLayoutJustifyContentStart 从前往后排列
    • ASStackLayoutJustifyContentCenter 居中排列
    • ASStackLayoutJustifyContentEnd 从后往前排列
    • ASStackLayoutJustifyContentSpaceBetween 间隔排列,两端无间隔
    • ASStackLayoutJustifyContentSpaceAround 间隔排列,两端有间隔
  4. alignItems : 交叉轴上的排列方式,有五个可选值:
    • ASStackLayoutAlignItemsStart 从前往后排列
    • ASStackLayoutAlignItemsEnd 从后往前排列
    • ASStackLayoutAlignItemsCenter 居中排列
    • ASStackLayoutAlignItemsStretch 拉伸排列
    • ASStackLayoutAlignItemsBaselineFirst 以第一个文字元素基线排列(主轴是横向才可用)
    • ASStackLayoutAlignItemsBaselineLast 以最后一个文字元素基线排列(主轴是横向才可用)
  5. children : 包含的视图。数组内元素顺序同样代表着布局时排列的顺序,所以需要注意

主轴的方向设置尤为重要,如果主轴设置的是 ASStackLayoutDirectionVertical , 那么 justifyContent 各个参数的意义就是:

  • ASStackLayoutJustifyContentStart 从上往下排列
  • ASStackLayoutJustifyContentCenter 居中排列
  • ASStackLayoutJustifyContentEnd 从下往上排列
  • ASStackLayoutJustifyContentSpaceBetween 间隔排列,两端无间隔
  • ASStackLayoutJustifyContentSpaceAround 间隔排列,两端有间隔

alignItems 就是:

  • ASStackLayoutAlignItemsStart 从左往右排列
  • ASStackLayoutAlignItemsEnd 从右往左排列
  • ASStackLayoutAlignItemsCenter 居中排列
  • ASStackLayoutAlignItemsStretch 拉伸排列
  • ASStackLayoutAlignItemsBaselineFirst 无效
  • ASStackLayoutAlignItemsBaselineLast 无效

对于子视图间距不一样的布局方法,后面实战中会讲到。

ASWrapperLayoutSpec

填充整个视图

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{    
    ASWrapperLayoutSpec *wrapperLayout = [ASWrapperLayoutSpec wrapperWithLayoutElement:self.childNodeA];
    return wrapperLayout;
}

布局实战

案例一

简单的文件覆盖在图片上,文字居中。

- (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;
}
  1. ASWrapperLayoutSpec 把图片铺满整个视图
  2. ASCenterLayoutSpec 把文字居中显示
  3. ASOverlayLayoutSpec 把文字覆盖到图片上

注意第三步就是之前提到的 ASOverlayLayoutSpec / ASBackgroundLayoutSpec 的作用:用于组合两个 Element

案例二

这个是 轻芒阅读 (豌豆荚一览) APP 内 AppSo 频道 Cell 的布局,应该也是比较典型的布局之一。为了方便理解先给各个元素定一下名称,从上至下,从左往右分别是:

  • coverImageNode // 大图
  • titleNode // 标题
  • subTitleNode // 副标题
  • dateTextNode // 发布时间
  • shareImageNode // 分享图标
  • shareNumberNode // 分享数量
  • likeImageNode // 喜欢图标
  • likeNumberNode // 喜欢数量
- (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 的这套布局方式必须遵守 从里到外 的布局原则,使用起来才会得心应手。

  1. 根据布局的原则,首先利用 ASStackLayoutSpec 布局 分享图标分享数量喜欢图标喜欢数量
  2. 还是通过 ASStackLayoutSpec 包装第一步的两个的布局得到 otherLayout 布局对象。
  3. 依然是 ASStackLayoutSpec 包装 otherLayout发布时间 。注意这里设置横向的排列方式 ASStackLayoutJustifyContentSpaceBetween 已到达两端布局的目的,最终返回 bottomLayout
  4. 由于 大图 是网络图片,对于 Cell 来说,子视图的布局必能能决定其高度(Cell 宽度是默认等于 TableNode 的宽度),所以这里必须设置 大图 的高度, ASRatioLayoutSpec 设置了图片的高宽比。
  5. 接下来布局应该就是 大图标题副标题bottomLayout 的一个纵向布局,可以发现这里的视图间距并不相同,这时候 spacingBeforespacingAfter 就会很有用,它们用来分别设置元素在主轴上的前后间距。 self.titleNode.style.spacingBefore = 12.0f; 意思就是 标题 相对于 大图 间距为 12。
  6. 最后通过一个 ASInsetLayoutSpec 设置一个边距。

可以看到不仅是 NodeASLayoutSpec 本身也可以作为布局元素,这是因为只要是遵守了 <ASLayoutElement> 协议的对象都可以作为布局元素。

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。