使用ASDK进行布局
在上一篇文章中,我们跟着源码,走了一遍ASDK异步绘制的流程,本篇将初步介绍/使用ASDK的布局系统,来探索并学习一下这个由Flexbox衍生而来的布局系统。
如果你从未了解过ASDK的布局系统,那么阅读官方文档或者是通过这个小游戏来入门,会是一个不错的选择。
本文基于AsyncDisplayKit 1.9.90,即将发布的2.0版本可能会有变动
Getting Started
Scott Goodson在介绍ASDK的各种宣讲会中反复提到,UIKit是一个非常强大的库,为了优化性能而摒弃这个库,重新写一套UI绘制渲染系统是没必要的,因此,ASDK更像是对UIKit的上层封装,但在布局系统这方面,ASDK则是完全丢弃了苹果的Autolayout或是VFL,转而自己实现了一套布局系统,在这套布局系统中,有以下几个核心概念:
1.ASLayoutable Protocol
顾名思义,可以被Layout的对象需要遵循这个protocol,协议的核心是结合各项配置,实现- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize;
方法,返回一个ASLayout
对象供布局系统布局。ASDisplayNode及其子类、ASLayoutSpec及其子类,都遵循这个协议。
2.ASLayout
存储着ASDK可以“理解”的布局信息,供布局系统进行布局,这个类一般不会直接接触到(除非你需要自己实现一个LayoutSpec),基本可以理解为,这个类存储了以下三个重要信息:**layout信息所作用的对象(layoutableObject)、layout的位置(position)、layout的大小(size)**。
3.ASLayoutSpec
ASLayout类存储着布局的信息,但若所有布局都要靠我们自己创建ASLayout对象,那几乎也就和纯手算然后setFrame没有太大区别了,ASLayoutSpec
及其子类可以看做是一个上层接口,让开发者不必考虑复杂的构建ASLayout
对象的过程,而通过这些LayoutSpec类来构造自己的布局,这个类及其子类将会是我们在开发的时候见到最多的。
理解ASLayoutSpec
利用ASDK的布局引擎所能获得的性能提升是不言而喻的,相比起在storyboard中拉几个constraints,亦或是借用Masonry在代码中完成约束的建立,ASDK的布局系统还是稍稍复杂一些,先来Demo最终的效果:
Demo最终展现了一系列人物信息,并在点击”View Photo”按钮后,下载一张“高清写真”,按比例显示出来,同时,cell也相应的自动增高。
在单纯利用UIKit布局的情况下,我们通常会这样做:从Storyboard中拉一个tableView,而后拉一个prototype cell,拖入UIImageView、UIButton、UILabel等控件,创建约束,而后创建一个UITableViewCell子类,拉取outlet,并在程序运行时为cell加载数据,为了让cell能自适应高度,我们可能还需要为estimatedRowHeight进行赋值等等。
而使用ASDK的布局引擎,我们大致要做如下工作:
为了让ASDK最大程度的进行性能优化,将UITableView转为ASTableNode,因为只有在诸如ASTableNode这样的NodeContainer中,其中的content才能正确获得Intelligent Preloading的相关状态更新,然后,我们创建一个ASCellNode子类,来完成我们的人物头像、信息、按钮以及动态下载图片的逻辑。
首先,我们在初始化的UIViewController子类中,加入ASTableNode:
1 | - (void)viewDidLoad { |
ASDK为UIView加入了一个Category,加入了addSubnode:
接口,但对于一般的UIView实例而言,这里和[view addSubview:node.view]
并无区别。
而后,就像我们实现UITableViewDataSource
一样,我们实现以下几个protocol方法(self.characters为数据源):
1 | -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{ |
其中,```objc
-(ASCellNodeBlock)tableView:nodeBlockForRowAtIndexPath:
1 |
|
上述两个方法唯一的区别就是:为了提高性能,nodeBlockForRowAtIndexPath:
方法的调用不一定在主线程,而nodeForRowAtIndexPath:
则一定是在主线程调用的,因此,如果你的cellNode的构造直接依赖于UIView/CALayer,那么你就只能调用后者来构造你的cellNode了。
接下来看看我们的cell类InfoCellNode
是如何实现的:
可以看到,除了Label和Button,我们有两个可能出现图片的地方,分别是头像和大图,两张图片都有一定程度的圆角,在UIKit中,实现圆角效果最常见的方法就是设置layer.cornerRadius
,然而这样的做法会带来不小的性能消耗,我们此处采用的是使用ASDK的ASImageNode所提供的imageModificationBlock
,来实现圆角效果,更多有关圆角的性能问题,官方文档的这里也有详细的说明。
我们来看看两个ASNetworkImageNode的初始化代码:(代码在InfoCellNode
的init
方法中)
1 | _headerNode = [ASNetworkImageNode new]; |
此处,我们为头像设置了一个defaultImage
属性,也就是当图片尚未下载完的时候所显示的Placeholder,注意,此处我注释掉了_photoNode.defaultImage = ...
的逻辑,主要是因为此处_photoNode需要根据下载下来的图片进行等比缩放,而在设置了defaultImage
之后,缩放功能出现了一些问题,已经有国外友人在github上面提了issue了。
我们在buttonNode响应事件中,如果图片正在下载或已经下载完成,我们直接让cellNode重新布局,否则将大图的URL赋值给_photoNode
,在其完成下载的delegate回调中,我们迫使cellNode重新布局:
1 | -(void)showPhoto{ |
另外需要注意的是,文本控件ASTextNode
没有.text
属性,也没有.textAlignment
,对其文本的设置统一成对其.attributedText
属性的设置,字体、字体颜色、对齐方式都在初始化NSAttributedString
的attributes
中进行设置。
ASDK 2.0之前的属性
.usesImplicitHierarchyManagement
或2.0之后的.automaticallyManagesSubnodes
提供了很好地Automatic Subnode Management,我们不需要显示的调用addSubnode:
,ASDK会在布局的时候自动检测,未被布局的node将不会被加入node hierarchy
接下来就是布局的过程了,我们再看看我们的布局:
我们可以先大致将整个Layout分为两部分:左边是头像以及按钮,右边是演员名字、角色名字,以及可能存在的大图。
对于左边我们希望:头像与按钮纵向排列,并在竖直方向上置顶。
对于右边:演员名、角色名、大图纵向排列。
同时:我们希望当Label中的文本非常长的时候,我们能够进行截断/换行。
首先,我们从简单的左侧布局做起,我们需要纵向布局,因此很自然想到使用ASStackLayoutSpec
来进行布局,同时,我们需要让头像控件保持固定大小,而不是根据实际图片尺寸缩放,因此我们要指定_headerNode
的大小,来看代码:
1 | -(ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ |
我们指定纵向Stack,则justifyContent
参数指定的就是竖直方向上该Stack Children布局的相对位置,而alignItems
则是水平方向上的对齐位置(这里的这些表现,和textAlignment很相似)。
在ASDK 2.0版本中,
.preferredFrameSize
被改为width
、height
等其他一系列属性,详见最新版头文件
对于右侧的布局,稍稍复杂一些:
1 | -(ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ |
类似于左侧布局,我们为node创建了一个纵向Stack布局,我们根据当前_photoNode.image
是否非空(即是否下载完毕)来决定是否将其加入布局(此处其实可以将三个node放入同一个stack中,而不必进行嵌套)。
StackLayout中指定了.spaceBefore
,.spaceAfter
属性的children,将在对应的布局方向上获得前置、后置的间隔。
而对某个ASLayoutable
的对象指定.flexShrink
,.flexGrow
则是表示,该对象所处在的LayoutSpec位置不足/多余时,其是否会自动缩小/扩充。对于一个ASLayoutable
容器,它的缩小/扩充实质是对其children的缩小/扩充,而在它的children中,它仅仅会尝试压缩/扩充没有intrinsicSize
的child以及.flexShrink
,.flexGrow
同样被设置成YES
的child,此处我们将rightStack
设为自动缩放,其实质是对其children的自动缩放,而ASRatioLayoutSpec
是没有intrinsicSize
的,因此可以正常缩放,但infoStack
中的两个node都是ASTextNode
,他们的intrinsicSize
由其中的文本决定,因此此处,若不显示添加node.flexShrink = YES
,infoStack
将无法进行缩放。
ASDK 2.0之前,
.flexShrink
,.flexGrow
默认为NO
,2.0之后,将默认打开
最后,我们将左右拼合,塞入一个ASInsetLayout
中,就完成了cell的布局:
1 | -(ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ |
效果:
接着,我们将字体调整一下,看看是否能够正常显示字体过长的情况:
Wow~这可不是我们想要的,回头想一想,刚刚提到了,当空间不足时,rightStack
试图压缩其中的content,但是由于_actorNode
是有实际大小的,因此不能被压缩,我们只要加上_actorNode.flexShrink = YES;
,即可正常触发换行逻辑_(不过这些在ASDK 2.0之后,都不需要我们手工指定了)_:
而如果在text过长时,采用截取而非换行的方式,只需要将node.maximumNumberOfLines
设置为对应的最大行数,并在node.truncationMode
中设置截取样式,即可得到截取的效果_(图为maximumNumberOfLines = 1
, truncationMode = NSLineBreakByTruncatingMiddle
)_:
动画
以往利用UIKit+Autolayout布局,当需要动画展示布局变化的时候,我们大概是这样做的:
1 | [UIView animateWithDuration:1 |
但在ASDK的布局系统中,想要用动画展现布局的改变稍微麻烦一些,需要在相应的ASDisplayNode
子类中重写-(void)animateLayoutTransition:
方法,并将setNeedsLayout
方法替换为transitionLayoutWithAnimation:
,我在尝试添加动画的时候遇到了不少问题,而且相应的LayoutTransition API也会在ASDK 2.0版本中有较大的变化,届时我探索清楚之后,将再补充此部分内容。
本文中出现的代码可以在这里进行下载,若发现有不正确的地方,欢迎指正。