Layout

学习自Texture-Quickstart

因为UIKit的Auto Layout框架 在视图层次变得复杂的时候,布局成本会成倍增加。(已经在iOS12中修复)

Texture提供的layout的api相比有很多优点

  1. 与手动使用layout布局一样快速
  2. 布局可以在后台计算,因此不会影响用户交互
  3. 布局采用了 不可变数据结构声明。
  4. 布局结果是不可变的数据结构,就可以提前预计算并缓存,提高用户体验
  5. 可扩展

Layout Specs

layout specs并没有物理呈现。它通过了解这些子布局元素如何相互关联来充当其他布局元素的容器,完成对布局元素的位置排列。

Texture提供了一些ASLayoutSpec的子类,包括从插入一个简单的布局规范到在不同堆栈中布置元素的的复杂规范

Layout Elements

Layout spec包含Layout Elements,并且对LayoutElements进行整理

所有的ASDisplayNodeASLayoutSec遵循<ASLayoutElement>协议。意味着我们可以通过不同的node或者layout Specs来组成其它布局规范

<ASLayoutElement>协议有一些属性用于创建非常复杂的布局。

组合Layout Specs和Layout Elements 进行布局

Layout Specs

ASWrapperLayoutSpec

ASLayoutSpec的一个简单的子类,可以封装一个ASLayoutElement元素并且根据Element上设置的大小计算元素的布局

通常用于在layyoutSpecThatFits:方法中返回一个单一的元素。这个元素可以设置大小布局信息。但是,如果你想设置postion而不仅仅只是大小,可以使用ASAbsoluteLayoutSpec

ASStackLayoutSpec

这个spec是最有用的。ASStackLayoutSpec使用flexbox布局算法 决定子元素的位置和大小。

ASStackLayoutSpec有7个属性

  • direction: 指定stack方向。当指定horizontalAligment/verticalAligment属性时,会被重新解析,而造成justifyContentalignItems相应更新
  • spacing:每个子元素的间距
  • horizontalAligment:指定子元素如何水平对齐,根据堆栈方向,设置对齐方式会导致justifyContentalignItems更新。即使direction更改,此属性将会仍然有效。
  • verticalAligment
  • justifyContent: 指定在主轴上的对齐方式
  • aligItems: 横轴上的对齐方式
  • flexWrap: 是否子元素被堆栈为单行或者多行。默认为单行
  • aligContent:

注意

与CSS中的flex 默认方向不同 而且并没有flex参数

ASInsetLayoutSpec

ASInsetLayoutSpec将其constrainedSize.max减去insets之后得到的CGSize传递给子节点,一旦子节点去定了它的size,insetSpec将最终size作为子节点的sizemargin

如果将UIEdgeInsets中的一个值设置为INFINITY,则将只使用子节点的固有大小

ASOverlayLayoutSpec

ASOverlayLayoutSpec则会将上面子节点延伸,覆盖一个子节点

OverlayLayoutSpec的size根据字节点size计算,子节点是被覆盖的底层,然后将子节点的size作为constrainedSize传递给叠加的子节点。因此,被覆盖的子节点必须有固定大小或者明确设置的大小

ASBackgroundLayoutSpec

ASOverlayLayoutSpec刚好相反,其设置一个子节点内容,并将另外一个子节点拉伸为背景

ASBackgroundLayoutSpec的size根据子节点的size确定,子节点size作为constrainedSize传递给背景子节点。因此 自己子节点也必须有固定大小或者明确设置的size

ASCenterLayoutSpec

ASCenterLayoutSpec将其子节点的中心设置为最大constrainedSize的中心

如果ASCenterLayoutSpec的宽度和高度没有约定,则会缩放到和子节点高度和宽度一致

属性:

  • centeringOptions:决定如何在ASCenterLayoutSpec中居中,可选值包括NONE X Y XY
  • sizingOptions: 决定ASCenterLayoutSpec占用 多少空间,可选值为Default, minimun X, minimun Y, minimun XY

ASRatioLayoutSpec

以固定的宽高比来缩放子节点。这个规则必须传一个高度或者宽度给他作为constrainedSize,进行计算

使用ASRatioLayoutSpecASNetworkImageNodeASVideoNode提供固有大小是非常常见的,因为两者在内容从服务器返回之前没有固定大小

ASRelativeLayoutSpec

ASAbsoluteLayoutSpec

通过设置他们的layoutPosition属性来指定其子节点的横纵坐标。

属性:

  • sizing: 确定ASAbsoluteLayoutSpec将占用多少空间,可选值Default, Size to Fit

ASLayoutSpec

所有布局规则的父类,负责处理和管理所有的子类,也可以用来创建自定义布局规则。不建议自定义子类,如果有这方面需求可以将提供的布局规则进行组合来实现

ASLayoutSpec中应用了.flexShrink.flexGrow,在ASStackLayoutSpec作为一个spacer和其它节点一起使用

Layout Element 属性

ASStackLayoutElement 属性

只有在ASStackLayoutsubnode上生效

  • .style.spacingBefore

    CGFloat类型,direction与前一个node的间隔

  • .style.spacingAfter

    CGFloat类型,direction与后一个node的间隔

  • .style.flexGrow

    Bool类型,子节点尺寸总和小于minimum 即存在剩余空间时,是否放大

  • style.flexShrink

    Bool类型,子节点综合大于maximum,即空间不足时,是否缩小

  • .style.flexBasis
    ASDimension类型,在应用flexGrow/flexShrink属性 并且分配剩余空间之前,以堆栈水平或垂直尺寸指定此对象的初始大小

  • .style.alignSelf
    ASStackLayoutAlignSelf类型, 指定对象在次轴方向上的布局,会覆盖alignItems。可选值有ASStackLayoutAlignSelfAuto, ASStackLayoutAlignSelfStart, ASStackLayoutAlignSelfEnd, ASStackLayoutAlignSelfCenter, ASStackLayoutAlignSelfStretch

  • .style.ascender
    CGFloat类型,用于基线对齐,描述对象从顶部到其基线的距离

  • .style.descender
    CGFloat类型,英语基线对齐,描述对象从基线到底部距离

ASAbsoluteLayoutElement Properties

只在ASAbsolute的subnode中才能生效

.style.layoutPosition

CGFloat类型,描述该对象在ASAbsoluteLayoutSpec父规则中的位置

ASLayoutElement Properties

适用于所有布局元素

  • .style.width
    ASDimension类型,width描述内容区域的宽度。默认值为ASDimensionAuto
    minWidthmaxWidth属性会覆盖width

  • .style.height
    ASDImension类型,height描述内容区域的高度。默认值为ASDimensionAuto
    minHeightmaxHeight属性会覆盖height

  • style.minWidth
    ASDImension类型,用于设置一个特定布局元素的最小宽度。默认值为ASDimensionAuto

  • style.maxWidth
    ASDImension类型,用于设置一个特定布局元素的最大宽度。默认值为ASDimensionAuto

  • .style.minHeight
    ASDImension类型,用于设置一个特定布局元素的最小高度。默认值为ASDimensionAuto

  • .style.maxHeight
    ASDImension类型,用于设置一个特定布局元素的最大高度。默认值为ASDimensionAuto

  • .style.preferredSize
    CGSize类型, 建议布局元素的size。minSize和maxSize会限制和覆盖该属性。如果未提供,默认会使用calculateSizeThatFits:方法提供的固有大小

    该属性是可选的,但是对于没有固定大小或需要用于固有大小不同的size进行布局的节点,则必须指定preferredSizepreferredLayoutSize中一个,比如这个属性可以在ASImageNode上设置,使这个节点的size和图片的size不同

    注意

    当size的宽度和高度时相对值时 调用getter进行断言

  • .style.minSize
    CGSize类型,可选属性,为布局元素提供最小尺寸,

  • .style.maxSize
    CGSize类型,可选属性,为布局元素提供最大尺寸

  • .style.preferredLayoutSize
    ASLayoutSize类型,为布局提供建议的size。
    使用百分比而不是点来指定布局。

  • .style.minLayoutSize
    ASLayoutSize类型,可选属性,为布局提供最小相对尺寸

  • .style.maxLayoutSize
    ASLayoutSize类型,可选属性,为布局提供最大相对尺寸

Layout API Sizing

ASDimension

为一个CGFloat表示一个pt值、一个百分比或者一个自动值,这个单位允许一个API同时使用固定值或者相对值

// 返回一个相对值
ASDimensionMake("50%")
ASDimensionMakeWithFraction(0.5)

// 返回一个 pt 值
ASDimensionMake("70pt")
ASDimensionMake(70)
ASDimensionMakeWithPoints(70)

CGSize、ASLayoutSize

ASLayoutSize类似于CGSize,只是其高度和宽度可以同时使用pt值或百分比,宽度和高度是独立的,它们的值类型可以不同。

允许同一个的API采用绝对值和相对值

ASLayoutSizeMake(ASDimension width, ASDimension height);

//例如
ASDimension width = ASDimensionMake(ASDimensionUnitAuto, 0);  
ASDimension 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并没有提供机制 绑定最大和最小的Size,因此,为了支持,创建了ASSizeRange

ASSizeRange通常用于layout的API内部,作为输入传递给layoutSpecThatFits:的constrainedSize值是ASSizeRange

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize;

传递的constrainedSize是node最合适的最大和最小尺寸,constrainedSize中包含的最小和最大CGSize可用于调整节点的布局元素的大小

Layout Transition API

参考Layout Transition API

帮助我们使所有动画 变得简单,甚至将一个试图集转为另外一个完全不同的视图集

注意

使用Layout Transition API必须使用自动节点管理功能

Layout 之间的动画

在这个过程中没有使用addSubnode:removeFromSupernode:。 Layout Transition API 会分析旧布局和新布局之间节点层次结构的差异,通过自动子节点管理隐式的执行节点插入和删除。

通过更新属性,来让展示不同的lauout specs。并且,调用transitionLayoutWithAnimation:调用动画方法。默认实现的API中,布局会重新计算布局,并且调整子节点的大小和位置,而不设置动画。此时我们需要自定义动画block,当通过transitionLayoutWithAnimation计算出新的布局后,会调用- (void)animateLayoutTransition:(id<ASContextTransitioning>)context我们在其中自定义动画

- (void)animateLayoutTransition:(id<ASContextTransitioning>)context
{
  if (self.fieldState == SignupNodeName) {
    CGRect initialNameFrame = [context initialFrameForNode:self.ageField];
    initialNameFrame.origin.x += initialNameFrame.size.width;
    self.nameField.frame = initialNameFrame;
    self.nameField.alpha = 0.0;
    CGRect finalAgeFrame = [context finalFrameForNode:self.nameField];
    finalAgeFrame.origin.x -= finalAgeFrame.size.width;
    [UIView animateWithDuration:0.4 animations:^{
      self.nameField.frame = [context finalFrameForNode:self.nameField];
      self.nameField.alpha = 1.0;
      self.ageField.frame = finalAgeFrame;
      self.ageField.alpha = 0.0;
    } completion:^(BOOL finished) {
      [context completeTransition:finished];
    }];
  } else {
    CGRect initialAgeFrame = [context initialFrameForNode:self.nameField];
    initialAgeFrame.origin.x += initialAgeFrame.size.width;
    self.ageField.frame = initialAgeFrame;
    self.ageField.alpha = 0.0;
    CGRect finalNameFrame = [context finalFrameForNode:self.ageField];
    finalNameFrame.origin.x -= finalNameFrame.size.width;
    [UIView animateWithDuration:0.4 animations:^{
      self.ageField.frame = [context finalFrameForNode:self.ageField];
      self.ageField.alpha = 1.0;
      self.nameField.frame = finalNameFrame;
      self.nameField.alpha = 0.0;
    } completion:^(BOOL finished) {
      [context completeTransition:finished];
    }];
  }
}

ASContextTransitioning中包含了过渡前和过度后的node状态信息。
动画完成后必须调用[context completeTransition:finished];,这会使其内部执行必要步骤,将计算的布局变为当前布局

在实现animateLayoutTransition:之前插入了节点,这是在开始动画之前手动管理层次结构的好地方。在context对象上调用completeTransition:之后,将在didCompleteLayoutTransition:中执行删除。如果您需要手动执行删除,请覆盖didCompleteLayoutTransition:并执行自定义操作。请注意,这将覆盖默认行为,建议调用super或遍历上下文对象中的RemovedSubnodes getter以执行清理。

将NO传递给transitionLayoutWithAnimation:仍将通过[context isAnimated]属性设置为NO的animateLayoutTransition:didCompleteLayoutTransition:实现运行。您可以选择如何处理此案件(如果有的话)。提供默认实现的一种简单方法是调用super

- (void)animateLayoutTransition:(id<ASContextTransitioning>)context
{
  if ([context isAnimated]) {
    // perform animation
  } else {
    [super animateLayoutTransition:context];
  }
}

限制大小更改动画

当只是想对node的边界更改作出相应,并为其重新计算layout 动画效果。此时可以在节点上调用transitionLayoutWithSizeRange:animated:

transitionLayoutWithAnimation:类似,但是如果传递的ASSizeRange和当前的相同,则不会触发动画,这对旋转视图好更改控制器大小非常有效果:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
    [self.node transitionLayoutWithSizeRange:ASSizeRangeMake(size, size) animated:YES];
  } completion:nil];
}