Adds support for using UIGraphicsImageRenderer in ASTextNode. (#1384)

* Adds support for using UIGraphicsImageRenderer in ASTextNode.

In many cases this reduces the backing store of text nodes by 1/2.

* Guard for UIGraphicsRenderer availability.

* Comma
This commit is contained in:
Garrett Moon 2019-03-09 07:55:46 -08:00 committed by Adlai Holler
parent 872e89b772
commit 9b80eabd8f
6 changed files with 80 additions and 23 deletions

View File

@ -26,7 +26,8 @@
"exp_skip_a11y_wait", "exp_skip_a11y_wait",
"exp_new_default_cell_layout_mode", "exp_new_default_cell_layout_mode",
"exp_dispatch_apply", "exp_dispatch_apply",
"exp_image_downloader_priority" "exp_image_downloader_priority",
"exp_text_drawing"
] ]
} }
} }

View File

@ -32,6 +32,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) {
ASExperimentalNewDefaultCellLayoutMode = 1 << 11, // exp_new_default_cell_layout_mode ASExperimentalNewDefaultCellLayoutMode = 1 << 11, // exp_new_default_cell_layout_mode
ASExperimentalDispatchApply = 1 << 12, // exp_dispatch_apply ASExperimentalDispatchApply = 1 << 12, // exp_dispatch_apply
ASExperimentalImageDownloaderPriority = 1 << 13, // exp_image_downloader_priority ASExperimentalImageDownloaderPriority = 1 << 13, // exp_image_downloader_priority
ASExperimentalTextDrawing = 1 << 14, // exp_text_drawing
ASExperimentalFeatureAll = 0xFFFFFFFF ASExperimentalFeatureAll = 0xFFFFFFFF
}; };

View File

@ -25,8 +25,8 @@ NSArray<NSString *> *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags
@"exp_skip_a11y_wait", @"exp_skip_a11y_wait",
@"exp_new_default_cell_layout_mode", @"exp_new_default_cell_layout_mode",
@"exp_dispatch_apply", @"exp_dispatch_apply",
@"exp_image_downloader_priority"])); @"exp_image_downloader_priority",
@"exp_text_drawing"]));
if (flags == ASExperimentalFeatureAll) { if (flags == ASExperimentalFeatureAll) {
return allNames; return allNames;
} }

View File

@ -18,6 +18,7 @@
#import <mutex> #import <mutex>
#import <tgmath.h> #import <tgmath.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h> #import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h> #import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h> #import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
@ -140,6 +141,9 @@ static ASTextKitRenderer *rendererForAttributes(ASTextKitAttributes attributes,
ASTextKitAttributes _rendererAttributes; ASTextKitAttributes _rendererAttributes;
UIColor *_backgroundColor; UIColor *_backgroundColor;
UIEdgeInsets _textContainerInsets; UIEdgeInsets _textContainerInsets;
CGFloat _contentScale;
BOOL _opaque;
CGRect _bounds;
} }
@end @end
@ -148,12 +152,18 @@ static ASTextKitRenderer *rendererForAttributes(ASTextKitAttributes attributes,
- (instancetype)initWithRendererAttributes:(ASTextKitAttributes)rendererAttributes - (instancetype)initWithRendererAttributes:(ASTextKitAttributes)rendererAttributes
backgroundColor:(/*nullable*/ UIColor *)backgroundColor backgroundColor:(/*nullable*/ UIColor *)backgroundColor
textContainerInsets:(UIEdgeInsets)textContainerInsets textContainerInsets:(UIEdgeInsets)textContainerInsets
contentScale:(CGFloat)contentScale
opaque:(BOOL)opaque
bounds:(CGRect)bounds
{ {
self = [super init]; self = [super init];
if (self != nil) { if (self != nil) {
_rendererAttributes = rendererAttributes; _rendererAttributes = rendererAttributes;
_backgroundColor = backgroundColor; _backgroundColor = backgroundColor;
_textContainerInsets = textContainerInsets; _textContainerInsets = textContainerInsets;
_contentScale = contentScale;
_opaque = opaque;
_bounds = bounds;
} }
return self; return self;
} }
@ -526,31 +536,74 @@ static NSArray *DefaultLinkAttributeNames() {
return [[ASTextNodeDrawParameter alloc] initWithRendererAttributes:[self _locked_rendererAttributes] return [[ASTextNodeDrawParameter alloc] initWithRendererAttributes:[self _locked_rendererAttributes]
backgroundColor:self.backgroundColor backgroundColor:self.backgroundColor
textContainerInsets:_textContainerInset]; textContainerInsets:_textContainerInset
contentScale:_contentsScaleForDisplay
opaque:self.isOpaque
bounds:[self threadSafeBounds]];
} }
+ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing + (UIImage *)displayWithParameters:(id<NSObject>)parameters isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelled
{ {
ASTextNodeDrawParameter *drawParameter = (ASTextNodeDrawParameter *)parameters; ASTextNodeDrawParameter *drawParameter = (ASTextNodeDrawParameter *)parameters;
UIColor *backgroundColor = (isRasterizing || drawParameter == nil) ? nil : drawParameter->_backgroundColor;
if (drawParameter->_bounds.size.width <= 0 || drawParameter->_bounds.size.height <= 0) {
return nil;
}
UIImage *result = nil;
UIColor *backgroundColor = drawParameter->_backgroundColor;
UIEdgeInsets textContainerInsets = drawParameter ? drawParameter->_textContainerInsets : UIEdgeInsetsZero; UIEdgeInsets textContainerInsets = drawParameter ? drawParameter->_textContainerInsets : UIEdgeInsetsZero;
ASTextKitRenderer *renderer = [drawParameter rendererForBounds:bounds]; ASTextKitRenderer *renderer = [drawParameter rendererForBounds:drawParameter->_bounds];
BOOL renderedWithGraphicsRenderer = NO;
CGContextRef context = UIGraphicsGetCurrentContext(); if (AS_AVAILABLE_IOS_TVOS(10, 10)) {
ASDisplayNodeAssert(context, @"This is no good without a context."); if (ASActivateExperimentalFeature(ASExperimentalTextDrawing)) {
renderedWithGraphicsRenderer = YES;
CGContextSaveGState(context); UIGraphicsImageRenderer *graphicsRenderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(drawParameter->_bounds.size.width, drawParameter->_bounds.size.height)];
CGContextTranslateCTM(context, textContainerInsets.left, textContainerInsets.top); result = [graphicsRenderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
CGContextRef context = rendererContext.CGContext;
// Fill background ASDisplayNodeAssert(context, @"This is no good without a context.");
if (backgroundColor != nil) {
[backgroundColor setFill]; CGContextSaveGState(context);
UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy); CGContextTranslateCTM(context, textContainerInsets.left, textContainerInsets.top);
// Fill background
if (backgroundColor != nil) {
[backgroundColor setFill];
UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy);
}
// Draw text
[renderer drawInContext:context bounds:drawParameter->_bounds];
CGContextRestoreGState(context);
}];
}
} }
// Draw text if (!renderedWithGraphicsRenderer) {
[renderer drawInContext:context bounds:bounds]; UIGraphicsBeginImageContextWithOptions(CGSizeMake(drawParameter->_bounds.size.width, drawParameter->_bounds.size.height), drawParameter->_opaque, drawParameter->_contentScale);
CGContextRestoreGState(context);
CGContextRef context = UIGraphicsGetCurrentContext();
ASDisplayNodeAssert(context, @"This is no good without a context.");
CGContextSaveGState(context);
CGContextTranslateCTM(context, textContainerInsets.left, textContainerInsets.top);
// Fill background
if (backgroundColor != nil) {
[backgroundColor setFill];
UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy);
}
// Draw text
[renderer drawInContext:context bounds:drawParameter->_bounds];
CGContextRestoreGState(context);
result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
return result;
} }
#pragma mark - Attributes #pragma mark - Attributes

View File

@ -112,7 +112,7 @@ NS_ASSUME_NONNULL_BEGIN
@summary Delegate override to provide new layer contents as a UIImage. @summary Delegate override to provide new layer contents as a UIImage.
@param parameters An object describing all of the properties you need to draw. Return this from -drawParametersForAsyncLayer: @param parameters An object describing all of the properties you need to draw. Return this from -drawParametersForAsyncLayer:
@param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return.
@return A UIImage with contents that are ready to display on the main thread. Make sure that the image is already decoded before returning it here. @return A UIImage (backed by a CGImage) with contents that are ready to display on the main thread. Make sure that the image is already decoded before returning it here.
*/ */
+ (UIImage *)displayWithParameters:(nullable id<NSObject>)parameters + (UIImage *)displayWithParameters:(nullable id<NSObject>)parameters
isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock; isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock;

View File

@ -31,7 +31,8 @@ static ASExperimentalFeatures features[] = {
ASExperimentalSkipAccessibilityWait, ASExperimentalSkipAccessibilityWait,
ASExperimentalNewDefaultCellLayoutMode, ASExperimentalNewDefaultCellLayoutMode,
ASExperimentalDispatchApply, ASExperimentalDispatchApply,
ASExperimentalImageDownloaderPriority ASExperimentalImageDownloaderPriority,
ASExperimentalTextDrawing
}; };
@interface ASConfigurationTests : ASTestCase <ASConfigurationDelegate> @interface ASConfigurationTests : ASTestCase <ASConfigurationDelegate>
@ -57,7 +58,8 @@ static ASExperimentalFeatures features[] = {
@"exp_skip_a11y_wait", @"exp_skip_a11y_wait",
@"exp_new_default_cell_layout_mode", @"exp_new_default_cell_layout_mode",
@"exp_dispatch_apply", @"exp_dispatch_apply",
@"exp_image_downloader_priority" @"exp_image_downloader_priority",
@"exp_text_drawing"
]; ];
} }