Texture

本文首先会讲解如何使用 Texture,以前叫 AsyncDisplayKit,讲解其核心概念、布局、便捷工具和优化方式,然后会讲解如何同 IGListKit 结合,最后会简短说一下 Yoga 和 Flexbox。
核心概念
Node Container
要使用 Texture 的 Node Container 才能管理到 Node 的生命周期,这样才有预加载和异步加载的功能,反正肯定要用的:
- ASCollectionNode 对应 UIKit 的 UICollectionView
- ASPagerNode 对应 UIKit 的 UIPageViewController
- ASTableNode 对应 UIKit 的 UITableView
- ASViewController 对应 UIKit 的 UIViewController
ASViewController 的子类
以下是要实现 ASViewController 子类的注意事项,首先所有方法都在主线程调用:
init()
不要在此方法中调用 view 或 node.view,会强迫 view 被提前创建,
func loadView()
不要使用
func viewDidLoad()
可以访问 view 和 layer,设置只需要运行一次的代码
func viewWillLayoutSubviews()
bounds 变化的时候被调用,包括旋转、分屏、弹出键盘,以及视图层级的变化,在这里可以做一些布局的工作
Node
Node 是对 UIView 或 CALayer 的封装,最基本的 UI 单元,支持异步布局和加载:
- ASDisplayNode 对应 UIView
- ASCellNode 对应 ASCollectionNode 或 ASTableNode 的 Cell,没有对应 Header、Footer、SectionHeader、SupplementaryView 的 Node
- ASTextNode 对应 UILabel
- ASImageNode 对应 UIImage 本地图片
- ASNetworkImageNode 对应 UIImage 网络图片
- ASButtonNode 对应 UIButton
Node 的子类
init()
使用 nodeBlock 创建时,此方法会从后台线程调用,不用在此方法初始化任何 UIKit 的对象,以及使用 node 的 view 或 layer
func didLoad()
此方法在主线程调用,保证 view 已经加载好,且只调用一次,所以可以做一些针对 UIKit 对象的工作
func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
此方法在后台线程调用,主要做布局的运算,布局的结果不会被缓存,每次重新计算,不要在此方法中创建新的 Node
func layout()
此方法在主线程调用,布局已经计算完成,可以做一些显隐的控制,背景色之类的设置,如果通过 -initWithViewBlock: 使用了 UIView,这里可以修改其 frame
布局
Layout Specs
layout spec 只是用来做布局运算,并不会产生实际的 UI 元素,可以做为布局元素的容器
Layout Elements
ASDisplayNode 和 ASLayoutSpec 都遵循 ASLayoutElement 协议,所以布局时候可以把 layout spec 和 node 组合起来。
不同的 layout spec 都根据其自身的情况有特定的属性,常见的 layout spec 如下:
- ASWrapperLayoutSpec 简单包装一个 node
- ASStackLayoutSpec Flexbox 布局容器
- ASInsetLayoutSpec 四个方向的 Margin 布局容器
- ASOverlayLayoutSpec 覆盖在上面的布局容器,大小由下层决定
- ASBackgroundLayoutSpec 做为背景的布局容器,大小由上层决定
- ASCenterLayoutSpec 在容器中居中的布局容器,还可以设置四个方向的偏移
- ASRatioLayoutSpec 比例容器
- ASRelativeLayoutSpec 九宫格的容器,放置在九宫格的任意位置
- ASAbsoluteLayoutSpec 绝对布局的容器
具体的布局示例和上面布局容器示例代码:
其他
便捷工具
- Hit Test Slop 增加可点击区域
- Batch Fetching API 针对 ASTableView 或 ASCollectionView 滑动批量加在新内容
- Automatic Subnode Management 自动新增和删除子 node
- Image Modification Blocks 后台异步修改图片的 Block
- Placeholders 给 node 设置图片 placeholder
优化
- Layer Backing 设置 node 是 view backed 还是 layer backed
- Subtree Rasterization 压平视图层级为一个 layer
- Synchronous Concurrency 设置 node 为同步加载
- Corner Rounding 详细讲解了实现圆角的优化方案,即便不使用 Texture 也能受益,强烈推荐看一看
为什么界面会闪烁
node 都是异步加载,在加载的过程中,Texture 会在 node 显示的位置放置一个图片的 placeholder,加载完毕后,替换为真实内容,所以从 placeholder 变为真实内容就会看到闪烁,再次 reload 这个 node 也会重复上面的过程,同样会闪烁。
node 中实现如下方法可以定制 placeholder
class MyNode: ASDisplayNode {
override func placeholderImage() -> UIImage? {
return nil
}
}
针对局部的变化,为了避免闪烁,可以采用 Synchronous Concurrency
IGListKit
之前介绍过的 IGListKit 可以和 Texture 结合起来,IGListKit 解决数据的 Diffing,Texture 解决 UI 加载和布局:
extension CartViewController: ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return list
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
if object as? CartItem != nil {
return CartItemSectionController()
}
return ListSectionController()
}
func emptyView(for listAdapter: ListAdapter) -> UIView? {
return nil
}
}
ListAdapter 的实现并没有什么特别的
class CartItemSectionController: ListSectionController {
private var item: CartItem!
override func didUpdate(to object: Any) {
item = object as? CartItem
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
return ASIGListSectionControllerMethods.cellForItem(at: index,
sectionController: self)
}
}
extension CartItemSectionController: ASSectionController {
func nodeBlockForItem(at index: Int) -> ASCellNodeBlock {
return { [unowned self] in
return CartItemCell(item: self.item)
}
}
}
主要利用 ASIGListSectionControllerMethods.cellForItem(at: index, sectionController: self) 将 IGListKit 和 Texture 结合起来, nodeBlockForItem 中返回的 CartItemCell 就是 Texture 的 ASCellNode,按照 Node 子类化的方式实现就可以了
更完整的示例:IGListKit-AsyncDisplayKit-Example
Yoga
使用了 Texture 对 Flexbox 布局的支持,是不是感觉很方便,如果只是想在 iOS 中使用 Flexbox 布局,可以使用 Yoga: