Texture 便捷方法
这是 Texture 文档系列翻译,其中结合了自己的理解和工作中的使用体会。如果哪里有误,希望指出。
Hit Test Slop
ASDisplayNode具有类型为UIEdgeInsets的hitTestSlop属性。当将其设置为正值时,缩小点击范围;设置为负值时,扩大点击范围。
所有 node 均继承自ASDisplayNode,因此所有 node 均可以使用hitTestSlop属性。
node 获取触摸事件的能力受父 node 尺寸、hitTestSlop限制,如果子 node 想要超出父 node 尺寸,则需要扩大父 node hitTestSlop以包含 child node 需要响应触摸事件区域。
用途
如果控件高度不足44 point(推荐的最小点击范围),则可以计算差值,使用hitTestSlop属性设置负值扩大点击区域。
ASTextNode *textNode = [[ASTextNode alloc] init];
CGFloat padding = (44.0 - button.bounds.size.height)/2.0;
textNode.hitTestSlop = UIEdgeInsetsMake(-padding, 0, -padding, 0);
批量拉取
Texture 的批量拉取(batch fetching api)功能可以很方便的拉取数据。UIKit通常在scrollViewDidScroll:方法中实现批量拉取功能,Texture 提供了一种更易用的拉取机制。
默认情况下,当用户滑动到距离 table、collection 内容末尾两屏幕时,将尝试拉取更多数据。
如果需要配置触发拉取的距离,只需设置ASTableNode、ASCollectionNode的leadingScreensForBatching属性。
tableNode.leadingScreensForBatching = 3.0; // overriding default of 2.0
批量拉取 delegate
在以下方法中决定是否执行批量拉取:
// ASTableNode
- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode
{
if (_weNeedMoreContent) {
return YES;
}
return NO;
}
// ASCollectionNode
- (BOOL)shouldBatchFetchForCollectionNode:(ASCollectionNode *)collectionNode
{
if (_weNeedMoreContent) {
return YES;
}
return NO;
}
当进入到需要拉取的区域时会触发上述方法。如果有更多数据则返回YES,进行拉取;反之,不拉取。
如果未实现上述方法,在进入拉取区域时会通知其
asyncDelegate。
如果上述方法返回了YES,则会调用下面的方法:
// ASTableNode
-tableNode:willBeginBatchFetchWithContext:
// ASCollectionNode
-collectionNode:willBeginBatchFetchWithContext:
在上述方法中执行拉取工作。比如网络 API、本地数据库。
- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context
{
// Fetch data most of the time asynchronously from an API or local database
NSArray *newPhotos = [SomeSource getNewPhotos];
// Insert data into table or collection node
[self insertNewRowsInTableNode:newPhotos];
// Decide if it's still necessary to trigger more batch fetches in the future
_stillDataToFetch = ...;
// Properly finish the batch fetch
[context completeBatchFetching:YES];
}
上述API会在后台线程调用。如需在主线程执行任务,则应将其调度到主线程。
拉取完成后,必须告知 Texture 该过程已经完成。为此,需要使用参数context调用completeBatchFetching:方法,且为completeBatchFetching:方法传入YES。只有传入YES,再次需要拉取时才会尝试拉取。
可以查看以下部分demo了解具体使用:
自动节点管理
想要使用 Texture 布局动画,必须开启自动节点管理(Automic Subnode Management,简称ASM)。即使没有使用 Texture 布局动画,开启 ASM 也可以减少代码量。
开启 ASM 后,无需调用addSubnode:、removeFromSuperNode方法。添加、移除完全由layoutSpecThatFits:方法决定。
示例
示例代码来自ASDKgram,其中ASCellNode创建了一个简单的照片流。
下面的代码1使用了熟悉的addSubNode:模式,代码2使用了 automatic subside management。如下所示:
// 代码1
- (instancetype)initWithPhotoObject:(PhotoModel *)photo {
self = [super init];
if (self) {
_photoModel = photo;
_userAvatarImageNode = [[ASNetworkImageNode alloc] init];
_userAvatarImageNode.URL = photo.ownerUserProfile.userPicURL;
[self addSubnode:_userAvatarImageNode];
_photoImageNode = [[ASNetworkImageNode alloc] init];
_photoImageNode.URL = photo.URL;
[self addSubnode:_photoImageNode];
_userNameTextNode = [[ASTextNode alloc] init];
_userNameTextNode.attributedString = [photo.ownerUserProfile usernameAttributedStringWithFontSize:FONT_SIZE];
[self addSubnode:_userNameTextNode];
_photoLocationTextNode = [[ASTextNode alloc] init];
[photo.location reverseGeocodedLocationWithCompletionBlock:^(LocationModel *locationModel) {
if (locationModel == _photoModel.location) {
_photoLocationTextNode.attributedString = [photo locationAttributedStringWithFontSize:FONT_SIZE];
[self setNeedsLayout];
}
}];
[self addSubnode:_photoLocationTextNode];
}
return self;
}
// 代码2
- (instancetype)initWithPhotoObject:(PhotoModel *)photo {
self = [super init];
if (self) {
self.automaticallyManagesSubnodes = YES;
_photoModel = photo;
_userAvatarImageNode = [[ASNetworkImageNode alloc] init];
_userAvatarImageNode.URL = photo.ownerUserProfile.userPicURL;
_photoImageNode = [[ASNetworkImageNode alloc] init];
_photoImageNode.URL = photo.URL;
_userNameTextNode = [[ASTextNode alloc] init];
_userNameTextNode.attributedString = [photo.ownerUserProfile usernameAttributedStringWithFontSize:FONT_SIZE];
_photoLocationTextNode = [[ASTextNode alloc] init];
[photo.location reverseGeocodedLocationWithCompletionBlock:^(LocationModel *locationModel) {
if (locationModel == _photoModel.location) {
_photoLocationTextNode.attributedString = [photo locationAttributedStringWithFontSize:FONT_SIZE];
[self setNeedsLayout];
}
}];
}
return self;
}
_userAvatarImageNode、_photoImageNode和_photoLocationLabel根据网络数据决定是否添加到视图中,应当何时添加呢?
ASM 会根据 cell 的ASLayoutSpec决定是否将其添加到 UI 中。
ASLayoutSpeck决定 UI 的视图层级,其由layoutSpecThatFits:返回。
你需要使用layoutSpecThatFits:构建视图,查看ASCellNode的部分布局代码:
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize {
ASStackLayoutSpec *headerSubStack = [ASStackLayoutSpec verticalStackLayoutSpec];
headerSubStack.flexShrink = YES;
if (_photoLocationLabel.attributedString) {
[headerSubStack setChildren:@[_userNameLabel, _photoLocationLabel]];
} else {
[headerSubStack setChildren:@[_userNameLabel]];
}
_userAvatarImageNode.preferredFrameSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); // constrain avatar image frame size
ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init];
spacer.flexGrow = YES;
UIEdgeInsets avatarInsets = UIEdgeInsetsMake(HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER, HORIZONTAL_BUFFER);
ASInsetLayoutSpec *avatarInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:avatarInsets child:_userAvatarImageNode];
ASStackLayoutSpec *headerStack = [ASStackLayoutSpec horizontalStackLayoutSpec];
headerStack.alignItems = ASStackLayoutAlignItemsCenter; // center items vertically in horizontal stack
headerStack.justifyContent = ASStackLayoutJustifyContentStart; // justify content to the left side of the header stack
[headerStack setChildren:@[avatarInset, headerSubStack, spacer]];
// header inset stack
UIEdgeInsets insets = UIEdgeInsetsMake(0, HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER);
ASInsetLayoutSpec *headerWithInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:headerStack];
// footer inset stack
UIEdgeInsets footerInsets = UIEdgeInsetsMake(VERTICAL_BUFFER, HORIZONTAL_BUFFER, VERTICAL_BUFFER, HORIZONTAL_BUFFER);
ASInsetLayoutSpec *footerWithInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:footerInsets child:_photoCommentsNode];
// vertical stack
CGFloat cellWidth = constrainedSize.max.width;
_photoImageNode.preferredFrameSize = CGSizeMake(cellWidth, cellWidth); // constrain photo frame size
ASStackLayoutSpec *verticalStack = [ASStackLayoutSpec verticalStackLayoutSpec];
verticalStack.alignItems = ASStackLayoutAlignItemsStretch; // stretch headerStack to fill horizontal space
[verticalStack setChildren:@[headerWithInset, _photoImageNode, footerWithInset]];
return verticalStack;
}
可以看到headerSubStack的 children 根据_photoLocationLabel字符串是否存在来决定。
更新ASLayoutSpec
如果某些变化改变了ASLayoutSpec,需要调用setNeedsLayout。其与动画中的transitionLayout:duration:0方法类似。可以在PhotoCellNode中查看如下:
[photo.location reverseGeocodedLocationWithCompletionBlock:^(LocationModel *locationModel) {
// check and make sure this is still relevant for this cell (and not an old cell)
// make sure to use _photoModel instance variable as photo may change when cell is reused,
// where as local variable will never change
if (locationModel == _photoModel.location) {
_photoLocationLabel.attributedText = [photo locationAttributedStringWithFontSize:FONT_SIZE];
[self setNeedsLayout];
}
}];
构建好的ASLayoutSpec将自动添加、移除或设置动画。
可以查看ASDKgramdemo了解具体布局过程,你会发现编写ASCellNode非常简单,该布局会根据大量单独数据自行调整。
该示例非常简单,但此功能有许多更强大的用途。
当 node 开启了 ASM 后,将不能调用
addSubnode:和removeFromSuperNode方法,否则会抛出 A flattened layout must consist exclusively of node sublayouts 的异常。
反转 inversion
ASTableNode和ASCollectionNode有一个BOOL类型属性inverted,当设置为YES时会自动反转内容,以便从下到上进行布局,即第一个(indexPath 为(0, 0)) node 位于底部,而非像往常一样在顶部。这对于聊天应用非常方便,只需设置inverted属性。
开启 inversion 后,开发者只需调整ASTableNode和ASCollectionNode的contentInset属性。如下所示:
CGFloat inset = [self topBarsHeight];
self.tableNode.view.contentInset = UIEdgeInsetsMake(0, 0, inset, 0);
self.tableNode.view.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, inset, 0);
let inset = self.topBarsHeight
self.tableNode.view.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: inset, right: 0.0)
self.tableNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: inset, right: 0.0)
查看SocialAppLayout-Inverteddemo了解详细实现。
修改图像块 Image Modification Blocks
通常,修改图像外观的操作对于主线程来说是昂贵操作,将其移到后台线程更为高效。
通过将imageModificationBlock分配给 imageNode,可以定义一组转换。转换会异步修改图像。
_backgroundImageNode.imageModificationBlock = ^(UIImage *image) {
UIImage *newImage = [image applyBlurWithRadius:30
tintColor:[UIColor colorWithWhite:0.5 alpha:0.3]
saturationDeltaFactor:1.8
maskImage:nil];
return newImage ?: image;
};
//some time later...
_backgroundImageNode.image = someImage;
someImage 先异步修改,后分配给 imageNode 进行显示。
添加图像处理
利用imageModificationBlock添加图像处理是最高效的方式。如果提供了 block,则可以在显示阶段执行绘制工作。由于显示是在后台线程执行的,因此不会堵塞主线程。
在下面的代码中,在父视图init方法中初始化 avatar node,avatar node 头像需要为圆形。通过提供imageModificationBlock将头像转换为圆形。
- (instancetype)init
{
// ...
_userAvatarImageNode.imageModificationBlock = ^UIImage *(UIImage *image) {
CGSize profileImageSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT);
return [image makeCircularImageWithSize:profileImageSize];
};
// ...
}
实际的绘制代码添加到了UIImage的分类中,如下所示:
@implementation UIImage (Additions)
- (UIImage *)makeCircularImageWithSize:(CGSize)size
{
// make a CGRect with the image's size
CGRect circleRect = (CGRect) {CGPointZero, size};
// begin the image context since we're not in a drawRect:
UIGraphicsBeginImageContextWithOptions(circleRect.size, NO, 0);
// create a UIBezierPath circle
UIBezierPath *circle = [UIBezierPath bezierPathWithRoundedRect:circleRect cornerRadius:circleRect.size.width/2];
// clip to the circle
[circle addClip];
// draw the image in the circleRect *AFTER* the context is clipped
[self drawInRect:circleRect];
// get an image from the image context
UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
// end the image context since we're not in a drawRect:
UIGraphicsEndImageContext();
return roundedImage;
}
@end
imageModificationBlock方法非常方便,可以用于添加各种图像效果而无需额外的调用显示。例如:圆角、边框或其他覆盖。
占位符 Placeholders
ASDisplayNode 实现占位符
ASDisplayNode的子类可以实现placeholderImage方法,以提供覆盖内容的占位符,直到节点内容完成显示。要使用占位符,请设置placeholderEnabled = YES;,另外还可以选择实现placeholderFadeDuration。
对于渲染图片,使用 node 的calculatedSize属性。
placeholderImage函数会在后台线程调用,因此需要确保线程安全。[UIImage imageNamed:]在 iOS 9 及以后线程安全,如果需要支持更低版本系统,可以使用[UIImage imageWithContentsOfFile:]方法。
imageNamed:方法会先检查缓存中是否有所需图片,如果缓存中不存在所需图片,则从 asset catalog 或磁盘加载。系统可能清空缓存以释放内存,清空时只会移除在缓存中且未正在使用的图片。如果图片只显示一次,不想将图片加入到缓存中,则可以使用
imageWithContentsOfFile:方法加载图片。
UIImage+ASConvenience分类提供了创建占位图图片的方法,包括圆形、矩形等。
查看Placeholdersdemo了解具体使用。
ASNetworkImageNode 默认图片
除占位符,ASNetworkImageNode还有defaultImage属性。占位符一般是临时性的,默认图片可能永久存在。例如图片 URL 为nil,或加载失败。
推荐为头像设置默认图片,为图片设置占位符。
与UICollectionViewCell组合使用
ASCollectionNode提供了ASCellNode和UICollectionViewCell同时使用的机制,但UICollectionViewCell不会获得预加载、异步布局、异步渲染的性能提升,即使与其他ASCellNode组合使用在同一个ASCollectionNode中。
组合使用方便开发者尝试 Texture,而不必重写所有 cell。
想用组合使用UICollectionViewCell,需满足:
- 遵守
ASCollecitonDataSourceInterop协议,可选实现ASCollectionDelegateInterop协议。 - 在
viewDidLoad中使用collectionNode.view调用registerCellClass:,或注册一个onDidLoad:块。 - 在
collectionNode:nodeBlockForItemAtIndexPath:方法或collectionNode:nodeForItemAtIndexPath:方法中返回nil。需要注意的是,不能在nodeBlock中返回nil。 - 最后,必须实现提供 item 大小的方法。有以下两种方式:
-
UICollectionViewFlowLayout,实现collectionNode:constrainedSizeForItemAtIndexPath:方法。 - 自定义布局。设置
view.layoutInspector,并实现collectionView:constrainedSizeForNodeAtIndexPath:。
-
默认情况下,UICollectionViewDataSource只在未提供ASCellNode时使用。然而,如果开启了dequeuesCellsForNodeBackedItems,则会调用UICollectionViewDataSource协议内方法重用cell,并期望返回ASCollectionViewCells类型对象。
CustomCollectionViewdemo 演示了如何组合使用UIKitcell和ASCellNodes。
打开demo后确保kShowUICollectionViewCells为YES。在这个示例中,collectionNode:nodeBlockForItemAtIndexPath:会为3倍数 cell 返回 nil。当返回 nil 时,ASCollectionNode会自动调用collectionView:cellForItemAtIndexPath:方法。
- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath {
if (kShowUICollectionViewCells && indexPath.item % 3 == 1) {
// When enabled, return nil for every third cell and then cellForItemAtIndexPath: will be called.
return nil;
}
UIImage *image = _sections[indexPath.section][indexPath.item];
return ^{
return [[ImageCellNode alloc] initWithImage:image];
};
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
return [_collectionNode.view dequeueReusableCellWithReuseIdentifier:kReuseIdentifier forIndexPath:indexPath];
}
下一篇:Texture 性能优化
欢迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/Texture%20%E4%BE%BF%E6%8D%B7%E6%96%B9%E6%B3%95.md