0%

Texture框架介绍

Texture 控件

节点容器(Node Containers)

节点容器 等价于 UIKit
ASCollectionNode 代替 UICollectionView
ASPagerNode 代替UIPageViewController
ASTableNode 代替UITableView
ASViewController 代替UIViewController
ASNavigationController 代替UINavigationController,实现 ASVisibility 协议。
ASTabBarController 代替UITabBarController,实现 ASVisibility 协议。

为什么使用节点容器,不使用UIViewController

  • Node默认是异步布局/渲染,只有在需要将frame/contents等同步到UIView上才会回到主线程,使其空出更多的时间处理其他事件
  • Node自动管理其子节点实现智能预加载,这意味着节点的所有布局计算,数据读取,解码和渲染都将会异步完成,这就是为什么我们建议将节点放进节点容器中使用的原因。

节点(Node Subclasses)

节点 等价于 UIKit
ASDisplayNode 代替 UIView,所有的 Node 都继承自 ASDisplayNode。
ASCellNode 代替 UITableViewCell&UICollectionViewCell,需要和 ASTableNode,ASCollectionNode 和 ASPagerNode 共同使用。
ASScrollNode 代替 UIScrollView,这个节点对于创建自定义的,包含其他节点的可滚动区域非常有用。
ASEditableTextNode 代替 UITextView。
ASTextNode 代替 UILabel。
ASImageNode 代替 UIImage。
ASNetworkImageNode 代替 UIImage。
ASMultiplexImageNode 代替 UIImage。
ASVideoNode 代替 AVPlayerLayer。
ASVideoPlayerNode 代替 UIMoviePlayer。
ASControlNode 代替 UIControl。
ASButtonNode 代替 UIButton。
ASMapNode 代替 MKMapView。

继承关系

ASDisplayNode

  • ASCellNode
    • ASTextCellNode
  • ASCollectionNode
    • ASPagerNode
  • ASControlNode
    • ASButtonNode
    • ASImageNode
      • ASMapNode
      • ASMultiplexImageNode
      • ASNetworkImageNode
        • ASVideoNode
    • ASTextNode
  • ASEditableTextNode
  • ASScrollNode
  • ASTableNode
  • ASVideoPlayerNode

ASDisplayNode

异步绘制步骤

  1. 在ASDisplayNode.h中有相当多的注释,其中displaysAsynchronously属性大致描述了异步渲染的步骤:
/** Asynchronous rendering proceeds as follows:
* When the view is initially added to the hierarchy, it has -needsDisplay true.
* After layout, Core Animation will call -display on the _ASDisplayLayer
* -display enqueues a rendering operation on the displayQueue
* When the render block executes, it calls the delegate display method
(-drawRect:… or -display)
* The delegate provides contents via this method and an operation is added to
the asyncdisplaykit_async_transaction
* Once all rendering is complete for the current
asyncdisplaykit_async_transaction,
* the completion for the block sets the contents on all of the layers in the
same frame
*/
  1. ASDisplayNode还有一个属性shouldRasterizeDescendants。
    /**
    * @abstract Whether to draw all descendant nodes’ layers/views into this node’s
    > layer/view’s backing store.
    * @discussion
    * When set to YES, causes all descendant nodes’ layers/views to be drawn
    > directly into this node’s layer/view’s backing
    * store. Defaults to NO.
    * If a node’s descendants are static (never animated or never change attributes
    > after creation) then that node is a
    * good candidate for rasterization. Rasterizing descendants has two main
    > benefits:
    * 1) Backing stores for descendant layers are not created. Instead the layers
    > are drawn directly into the rasterized
    * container. This can save a great deal of memory.
    * 2) Since the entire subtree is drawn into one backing store, compositing and
    > blending are eliminated in that subtree
    * which can help improve animation/scrolling/etc performance.
    * Rasterization does not currently support descendants with transform,
    > sublayerTransform, or alpha. Those properties
    * will be ignored when rasterizing descendants.
    * Note: this has nothing to do with -[CALayer shouldRasterize], which doesn’t
    > work with ASDisplayNode’s asynchronous
    * rendering model.
    */
    当我们不需要分别关注单个CALayer,也不需要对他们进行操作时,就可以将所有的子node都合并到父node的backing store一并绘制,从而达到节省内存和提高性能的目的。

ASDisplayNode 对象方法

  • init 任何线程调用
    • 不应该在节点初始化方法中初始化任何 UIKit 对象,以及调用 node.layer node.view.x等与view 或 layer 有关的操作
    • touch事件或者手势绑定不要在这里执行
    • 这些事件应该在 didLoad 方法中进行。
  • didLoad 主线程调用
    • 当后台视图初始化完成时,它会被调用一次
    • 可以进行任何UI相关操作和初始化
    • 需要注意的是,这里是获取不到正确的frame的,获取frame要去layout方法
  • layoutSpecThatFits
    • 该方法定义了节点的布局,并在后台线程上进行了大量的计算。此方法是你声明、创建和修改 ASLayoutSpec 布局描述对象的地方,该对象描述了节点的 size,以及其子节点的 size 和 position,是你放置大部分布局代码的地方。
    • ASLayoutSpec 对象直到在此方法中返回前是可变的。 在这之后,这个对象将不可改变
    • 由于它在后台线程上运行,因此你不能在这个方法中调用 node.view 或 node.layer 以及它们的属性。
    • 此外,除非你明确知道自己在做什么,否则不要在此方法中创建其他节点
  • layout 主线程调用
    • 比较类似viewWillLayoutSubviews,在此方法中调用 super 将会使用 layoutSpec 对象计算布局,所有子节点都将计算其 size 和 position。
    • 适合进行hidden或者背景色等UI基本信息设置,但不包含布局设置
    • 你可以在 -layoutspec: 方法中设定背景颜色,但这可能会存在时序问题。
    • 如果要设置UIView相关对象,可以在layout中设置frame,尽量使用initWithViewBlock方法,放到后台进程
    • 官方举了个使用例子,colletionNode设置全屏适合在layout方法中调用

一些特有的东西

  • ASCellNode

    • 作用同等于 UITableViewCell 或 UICollectionViewCell,自带 indexPath 属性,不用注册cell
  • ASButtonNode

    @property (nonatomic, assign) CGFloat contentSpacing; // 设置图片和文字的间距
    @property (nonatomic, assign) ASButtonNodeImageAlignment imageAlignment;// 图片和文字的排列方式,
    /**
    ASButtonNodeImageAlignmentBeginning, // 图片在前,文字在后
    ASButtonNodeImageAlignmentEnd// 文字在前,图片在后
    */
  • ASNetworkImageNode

ASViewController

ASViewController 是一个常规的 UIViewController 子类,它具有管理节点的特殊功能。因为它是一个 UIViewController 子类,所以所有的方法都在主线程上被调用,并且你应该在主线程上创建至少一个 ASViewController。

ASViewController 对象方法

  • init
    • 这个方法在 ASViewController 的生命周期开始时被调用一次
    • 不要访问self.view或者self.node.view,这样会强迫视图被过早初始化,以免引发问题。
    • 自带initWithNode方法初始化,其自身管理node的方法和view类似
  • loadView
    • 官方不建议使用这个方法
  • viewDidLoad
    • 跟 UIViewController一样,这个方法在 -loadView 之后被执行
    • 可以访问 node.view 最早的方法,你可以在这份方法中任意修改 view 和 layer 或添加手势,这个方法在其所属的生命周期中,只会执行一次。
    • 布局代码不应该放在这个方法中,因为当界面重绘时,这里的代码不会被再次调用。UIViewController 中这个方法也是同样的,在这种方法中放置布局代码是一种不太好的做法,即使你的布局不会因为交互发生变化。
  • viewWillLayoutSubviews
    • 这个方法会与Node的 -layout 同时调用,它可能在 ASViewController 的生命周期中被多次调用
    • 当 ASViewController 的节点的边界发生改变,如旋转、分割屏幕、键盘弹出等行为,或者当视图的层次结构发生变化,如子节点添加、删除或改变大小时,这个方法将被调用。
    • 官方建议把布局逻辑写在这里(不强依赖于size的代码)
  • viewWillAppear / viewDidDisappear
    • 适合统计用户行为log
    • 适合进行Controller的动画操作

Texture 布局

ASLayout

@interface ASLayout : NSObject

@property (nonatomic, weak, readonly) id<ASLayoutable> layoutableObject; // 它所代表的布局元素
@property (nonatomic, readonly) CGSize size; // 元素的尺寸
@property (nonatomic, readwrite) CGPoint position; // 元素的位置
@property (nonatomic, readonly) NSArray<ASLayout *> *sublayouts; // 它所包含的sublayouts
@property (nonatomic, readonly) CGRect frame;

...

@end

可以看出,当一个node具备了确定的ASLayout对象时,它自身的布局也就随之确定了
只要Node能够计算出自己的ASLayout,父元素就可以完成对其的布局。这种方法(measureWithSizeRange:)将sizeThatFits和layoutSubviews结合在一起,一定程度上避免了相似代码的尴尬,但是计算上仍然是手动布局,不够简便。

ASLayoutSpec

  • 存储着ASDK可以“理解”的布局信息,供布局系统进行布局,这个类一般不会直接接触到(除非你需要自己实现一个LayoutSpec),基本可以理解为,这个类存储了以下三个重要信息:layout信息所作用的对象(layoutableObject)、layout的位置(position)、layout的大小(size)。
  • ASLayout类存储着布局的信息,但若所有布局都要靠我们自己创建ASLayout对象,那几乎也就和纯手算然后setFrame没有太大区别了,ASLayoutSpec及其子类可以看做是一个上层接口,让开发者不必考虑复杂的构建ASLayout对象的过程,而通过这些LayoutSpec类来构造自己的布局,这个类及其子类将会是我们在开发的时候见到最多的。
  • ASLayoutSpec只负责指定布局规则,而不关心其布局的具体是Node还是其他ASLayoutSpec
  • ASLayoutSpec 的作用更像是一个抽象类,在真正使用 ASDK 的布局引擎时,都不会直接使用这个类,而是会用类似 ASStackLayoutSpec、ASRelativeLayoutSpec、ASOverlayLayoutSpec 以及 ASRatioLayoutSpec 等子类。

Layout Types

2021-02-20-15167749195271

Layout继承关系

2021-02-20-15167751656197

ASLayoutSpec 与下面的所有的 Spec 类都是继承关系,在视图需要布局时,会调用 ASLayoutSpec 或者它的子类的 - measureWithSizeRange: 方法返回一个用于布局的对象 ASLayout。

几种布局规则

规则 描述
ASWrapperLayoutSpec 填充布局
ASStackLayoutSpec 盒子布局
ASInsetLayoutSpec 插入布局
ASOverlayLayoutSpec 覆盖布局
ASBackgroundLayoutSpec 背景布局
ASCenterLayoutSpec 中心布局
ASRatioLayoutSpec 比例布局
ASRelativeLayoutSpec 顶点布局
ASAbsoluteLayoutSpec 绝对布局

以上几种布局就不一一介绍了,具体可以看官方的DEMO

Layout Element 布局元素属性

  • ASStackLayoutElement Properties:只会在盒子布局中的的 subnodelayoutSpec 中生效;
  • ASAbsoluteLayoutElement Properties:只会在绝对布局中的的 subnodelayoutSpec 中生效;
  • ASLayoutElement Properties:适用于所有 NodelayoutSpec

ASStackLayoutElement Properties

请注意,以下属性只有在 ASStackLayoutsubnode上设置才会生效。

.style.spacingBefore

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

.style.spacingAfter

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

.style.flexGrow

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

.style.flexShrink

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

.style.flexBasis

ASDimension 类型,描述在剩余空间是均分的情况下,应用 flexGrowflexShrink 属性之前,该对象在盒子中垂直或水平方向的初始 size

.style.alignSelf

ASStackLayoutAlignSelf 类型,描述对象在十字轴的方向,此属性会覆盖 alignItems,可选值有:

  • ASStackLayoutAlignSelfAuto
  • ASStackLayoutAlignSelfStart
  • ASStackLayoutAlignSelfEnd
  • ASStackLayoutAlignSelfCenter
  • ASStackLayoutAlignSelfStretch

.style.ascender

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

.style.descender

CGFloat 类型,用于基线对齐,描述对象从基线到其底部的距离。

ASAbsoluteLayoutElement Properties

请注意,以下属性只有在 AbsoluteLayoutsubnode上设置才会生效。

.style.layoutPosition

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

ASLayoutElement Properties

请注意,以下属性适用于所有布局元素。

.style.width

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

.style.height

ASDimension 类型,height 属性描述了 ASLayoutElement 内容区域的高度。 minHeightmaxHeight 属性会覆盖 height,默认值为 ASDimensionAuto

.style.minWidth

ASDimension 类型,minWidth 属性用于设置一个特定布局元素的最小宽度。 它可以防止 width 属性的使用值小于 minWidth 指定的值,minWidth 的值会覆盖 maxWidthwidth。 默认值为 ASDimensionAuto

.style.maxWidth

ASDimension 类型,maxWidth 属性用于设置一个特定布局元素的最大宽度。 它可以防止 width 属性的使用值大于 maxWidth 指定的值,maxWidth 的值会覆盖 widthminWidth 会覆盖 maxWidth。 默认值为 ASDimensionAuto

.style.minHeight

ASDimension 类型,minHeight 属性用于设置一个特定布局元素的最小高度。 它可以防止 height 属性的使用值小于 minHeight 指定的值。 minHeight 的值会覆盖 maxHeightheight。 默认值为 ASDimensionAuto

.style.maxHeight

ASDimension 类型,maxHeight 属性用于设置一个特定布局元素的最大高度,它可以防止 height 属性的使用值大于 maxHeight 指定的值。 maxHeight 的值会覆盖 heightminHeight 会覆盖 maxHeight。 默认值为 ASDimensionAuto

.style.preferredSize

CGSize 类型, 建议布局元素的 size 应该是多少。 如果提供了 minSizemaxSize ,并且 preferredSize 超过了这些值,则强制使用 minSizemaxSize。 如果未提供 preferredSize,则布局元素的 size 默认为 calculateSizeThatFits: 方法提供的固有大小。

此方法是可选的,但是对于没有固有大小或需要用与固有大小不同的的 size 进行布局的节点,则必须指定 preferredSizepreferredLayoutSize 中的一个,比如没这个属性可以在 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 类型,为布局元素提供建议的相对 sizeASLayoutSize 使用百分比而不是点来指定布局。 例如,子布局元素的宽度应该是父宽度的 50%。 如果提供了可选的 minLayoutSizemaxLayoutSize,并且 preferredLayoutSize 超过了这些值,则将使用 minLayoutSizemaxLayoutSize。 如果未提供此可选值,则布局元素的 size 将默认是 calculateSizeThatFits: 提供的固有大小。

.style.minLayoutSize

ASLayoutSize 类型, 可选属性,为布局元素提供最小的相对尺寸, 如果提供,minLayoutSize 将会强制使用。 如果父级布局元素的 minLayoutSize 小于其子级的 minLayoutSize,则会强制使用子级的 minLayoutSize,并且其大小将扩展到布局规则之外。

.style.maxLayoutSize

ASLayoutSize 类型, 可选属性,为布局元素提供最大的相对尺寸。 如果提供,maxLayoutSize 将会强制使用。 如果父级布局元素的 maxLayoutSize 小于其子级的 maxLayoutSize,那么将强制使用子级的 maxLayoutSize,并且其大小将扩展到布局规则之外。

一些布局小贴士

  • 更新布局,记得调用setNeedsLayout(经过本人测试,这个方法,同时会刷新相关的所有的Node)
  • automaticallyManagesSubnodes 方法可以自动管理子Node,不用手动addSubNode(需要注意的是,要实现对应的布局才会添加到父视图里)
  • inverted 可以翻转整个tableNode、CollectionNode的NSIndexPath,最下方的cellNode变成NSIndexPath(0, 0)
  • imageModificationBlock 是ImageNode的block,可以做一个后期处理,比如圆角裁剪等
  • shouldRasterizeDescendants 子树光栅化,意味着类似VVebo的效果,一个点内的所有node层级都会绘制到一个layer上
  • neverShowPlaceholders 同步绘制cellNode,主线程会锁死,直到界面完成绘制
  • hitTestSlop属性可以扩大点击响应区域(但是同时,也会影响frame)
    • enableHitTestDebug ADK提供很多debug功能,这个是测试相应区域的,可响应区域会变成绿色

一些官方的建议

  • 常见错误
    • 禁止在-init:方法访问node.view
    • 确保在node block外部访问data source
    • viewBlocks避免循环引用
  • 常见概念误区
    • ASCellNode不会复用
    • LayoutSpecs会在每次layout被调用时执行
    • AsyncDisplayKit有自己的强大布局API,用法上和系统的API又很大区别
  • 常见性能问题场景
    • 避免使用cornerRadius属性(以及shadowPath,border, mask),CALayer的cornerRadius是一个性能杀手,他是CALayer上效率最低,渲染密度最高的属性之一,这些属性会出发离屏渲染,也就是说滚动的同时实时渲染,具体可以查看官方的圆角指南
    • ASDK不支持Auto Layout
    • ASDisplayNode的指针会保持alive(据说系统中当layer被添加到UIView的super layer上后,也会一直保留,即使layer的delegate被置为weak)
  • Corner Rounding 关于圆角性能
    • 官网有一篇圆角的分析和优化文章,具体可以看文档

参考链接