From 0d439a43b6eef8c720d28b5a93213074b827a7a6 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Mon, 24 Oct 2016 17:05:59 -0700 Subject: [PATCH] [ASTextNode] Add Fast-Paths For Text Measurement and Drawing (#2392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add test case for TextKit truncation style * Add fast path for text node measurement with default truncation * Use fast path more often * Reverse options order * Simplify implementation – no functional change * Share "isScaled" variable * Intersect with constrained rect * Add a failing test case for fast-path truncation * Add some more truncation tests, using slow path as reference image * Update the tests * In ASTextKitRenderer, intersect bounds with constrained rect * Add test case for TextKit truncation style * Add fast path for text node measurement with default truncation * Use fast path more often * Reverse options order * Simplify implementation – no functional change * Share "isScaled" variable * Intersect with constrained rect * Add a failing test case for fast-path truncation * Add some more truncation tests, using slow path as reference image * Update the tests * In ASTextKitRenderer, intersect bounds with constrained rect * Use maximumNumberOfLines property in text kit fast path Add reference images Disable fast-path for max-one-line case * Remove unneeded snapshot files --- AsyncDisplayKit/ASTextNode.mm | 2 +- AsyncDisplayKit/TextKit/ASTextKitRenderer.mm | 136 +++++++++++++----- AsyncDisplayKitTests/ASSnapshotTestCase.h | 3 + .../ASTextNodePerformanceTests.m | 2 +- .../ASTextNodeSnapshotTests.m | 19 ++- .../testThatFastPathTruncationWorks@2x.png | Bin 0 -> 4025 bytes .../testThatSlowPathTruncationWorks@2x.png | Bin 0 -> 4018 bytes .../testThatFastPathTruncationWorks@2x.png | Bin 0 -> 3978 bytes .../testThatSlowPathTruncationWorks@2x.png | Bin 0 -> 3974 bytes 9 files changed, 120 insertions(+), 42 deletions(-) create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 3de201d87e..f287eb931f 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -1356,7 +1356,7 @@ static NSAttributedString *DefaultTruncationAttributedString() } // If we've reached this point, both _additionalTruncationMessage and - // _truncationAttributedString are present. Compose them. + // _truncationAttributedText are present. Compose them. NSMutableAttributedString *newComposedTruncationString = [[NSMutableAttributedString alloc] initWithAttributedString:_truncationAttributedText]; [newComposedTruncationString replaceCharactersInRange:NSMakeRange(newComposedTruncationString.length, 0) withString:@" "]; diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm index 0beb67902c..4a6f5978c0 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm @@ -35,11 +35,20 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() return truncationCharacterSet; } +@interface ASTextKitRenderer() +/** + * This object is lazily created. It is provided to the NSAttributedString + * drawing methods used by the fast-paths in the size calculation and drawing + * instance methods. + */ +@property (nonatomic, strong, readonly) NSStringDrawingContext *stringDrawingContext; +@end + @implementation ASTextKitRenderer { CGSize _calculatedSize; BOOL _sizeIsCalculated; } -@synthesize attributes = _attributes, context = _context, shadower = _shadower, truncater = _truncater, fontSizeAdjuster = _fontSizeAdjuster; +@synthesize attributes = _attributes, context = _context, shadower = _shadower, truncater = _truncater, fontSizeAdjuster = _fontSizeAdjuster, stringDrawingContext = _stringDrawingContext; #pragma mark - Initialization @@ -50,6 +59,7 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() _constrainedSize = constrainedSize; _attributes = attributes; _sizeIsCalculated = NO; + _currentScaleFactor = 1; } return self; } @@ -106,6 +116,19 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() return _context; } +- (NSStringDrawingContext *)stringDrawingContext +{ + if (_stringDrawingContext == nil) { + _stringDrawingContext = [[NSStringDrawingContext alloc] init]; + + if (isinf(_constrainedSize.width) == NO && _attributes.maximumNumberOfLines > 0) { + ASDisplayNodeAssert(_attributes.maximumNumberOfLines != 1, @"Max line count 1 is not supported in fast-path."); + [_stringDrawingContext setValue:@(_attributes.maximumNumberOfLines) forKey:@"maximumNumberOfLines"]; + } + } + return _stringDrawingContext; +} + #pragma mark - Sizing - (CGSize)size @@ -144,9 +167,18 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() if (isinf(_constrainedSize.width) == NO && [_attributes.pointSizeScaleFactors count] > 0) { _currentScaleFactor = [[self fontSizeAdjuster] scaleFactor]; } - - __block NSTextStorage *scaledTextStorage = nil; + + // If we do not scale, do exclusion, or do custom truncation, we should just use NSAttributedString drawing for a fast-path. + if (self.canUseFastPath) { + CGRect rect = [_attributes.attributedString boundingRectWithSize:_constrainedSize options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:self.stringDrawingContext]; + // Intersect with constrained rect, in case text kit goes out-of-bounds. + rect = CGRectIntersection(rect, {CGPointZero, _constrainedSize}); + _calculatedSize = [self.shadower outsetSizeWithInsetSize:rect.size]; + return; + } + BOOL isScaled = [self isScaled]; + __block NSTextStorage *scaledTextStorage = nil; if (isScaled) { // apply the string scale before truncating or else we may truncate the string after we've done the work to shrink it. [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { @@ -161,15 +193,13 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() [[self truncater] truncate]; + CGRect constrainedRect = {CGPointZero, _constrainedSize}; + __block CGRect boundingRect; + // Force glyph generation and layout, which may not have happened yet (and isn't triggered by // -usedRectForTextContainer:). [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { [layoutManager ensureLayoutForTextContainer:textContainer]; - }]; - - CGRect constrainedRect = {CGPointZero, _constrainedSize}; - __block CGRect boundingRect; - [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { boundingRect = [layoutManager usedRectForTextContainer:textContainer]; if (isScaled) { // put the non-scaled version back @@ -181,13 +211,33 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() // TextKit often returns incorrect glyph bounding rects in the horizontal direction, so we clip to our bounding rect // to make sure our width calculations aren't being offset by glyphs going beyond the constrained rect. boundingRect = CGRectIntersection(boundingRect, {.size = constrainedRect.size}); - CGSize boundingSize = [_shadower outsetSizeWithInsetSize:boundingRect.size]; - _calculatedSize = CGSizeMake(boundingSize.width, boundingSize.height); + _calculatedSize = [_shadower outsetSizeWithInsetSize:boundingRect.size]; } - (BOOL)isScaled { - return (self.currentScaleFactor > 0 && self.currentScaleFactor < 1.0); + return (_currentScaleFactor > 0 && _currentScaleFactor < 1.0); +} + +- (BOOL)usesCustomTruncation +{ + // NOTE: This code does not correctly handle if they set `…` with different attributes. + return _attributes.avoidTailTruncationSet != nil || [_attributes.truncationAttributedString.string isEqualToString:@"\u2026"] == NO; +} + +- (BOOL)usesExclusionPaths +{ + return _attributes.exclusionPaths.count > 0; +} + +- (BOOL)canUseFastPath +{ + return self.isScaled == NO + && self.usesCustomTruncation == NO + && self.usesExclusionPaths == NO + // NSAttributedString drawing methods ignore usesLineFragmentOrigin if max line count 1, + // rendering them useless: + && (_attributes.maximumNumberOfLines != 1 || isinf(_constrainedSize.width)); } #pragma mark - Drawing @@ -198,11 +248,12 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() ASDisplayNodeAssertNotNil(context, @"This is no good without a context."); // This renderer may not be the one that did the sizing. If that is the case its truncation and currentScaleFactor may not have been evaluated. - // If there's any possibility we need to truncate or scale (e.g. width is not infinite, perform the size calculation. + // If there's any possibility we need to truncate or scale (i.e. width is not infinite), perform the size calculation. if (_sizeIsCalculated == NO && isinf(_constrainedSize.width) == NO) { [self _calculateSize]; } + bounds = CGRectIntersection(bounds, { .size = _constrainedSize }); CGRect shadowInsetBounds = [[self shadower] insetRectWithConstrainedRect:bounds]; CGContextSaveGState(context); @@ -210,35 +261,42 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() UIGraphicsPushContext(context); LOG(@"%@, shadowInsetBounds = %@",self, NSStringFromCGRect(shadowInsetBounds)); - - [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { - - NSTextStorage *scaledTextStorage = nil; - BOOL isScaled = [self isScaled]; - if (isScaled) { - // if we are going to scale the text, swap out the non-scaled text for the scaled version. - NSMutableAttributedString *scaledString = [[NSMutableAttributedString alloc] initWithAttributedString:textStorage]; - [ASTextKitFontSizeAdjuster adjustFontSizeForAttributeString:scaledString withScaleFactor:_currentScaleFactor]; - scaledTextStorage = [[NSTextStorage alloc] initWithAttributedString:scaledString]; + // If we use default options, we can use NSAttributedString for a + // fast path. + if (self.canUseFastPath) { + [_attributes.attributedString drawWithRect:shadowInsetBounds options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:self.stringDrawingContext]; + } else { + BOOL isScaled = [self isScaled]; + [[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { - [textStorage removeLayoutManager:layoutManager]; - [scaledTextStorage addLayoutManager:layoutManager]; - } - - LOG(@"usedRect: %@", NSStringFromCGRect([layoutManager usedRectForTextContainer:textContainer])); - NSRange glyphRange = [layoutManager glyphRangeForBoundingRect:CGRectMake(0,0,textContainer.size.width, textContainer.size.height) inTextContainer:textContainer]; - LOG(@"boundingRect: %@", NSStringFromCGRect([layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer])); - - [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; - [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; - - if (isScaled) { - // put the non-scaled version back - [scaledTextStorage removeLayoutManager:layoutManager]; - [textStorage addLayoutManager:layoutManager]; - } - }]; + NSTextStorage *scaledTextStorage = nil; + + if (isScaled) { + // if we are going to scale the text, swap out the non-scaled text for the scaled version. + NSMutableAttributedString *scaledString = [[NSMutableAttributedString alloc] initWithAttributedString:textStorage]; + [ASTextKitFontSizeAdjuster adjustFontSizeForAttributeString:scaledString withScaleFactor:_currentScaleFactor]; + scaledTextStorage = [[NSTextStorage alloc] initWithAttributedString:scaledString]; + + [textStorage removeLayoutManager:layoutManager]; + [scaledTextStorage addLayoutManager:layoutManager]; + } + + LOG(@"usedRect: %@", NSStringFromCGRect([layoutManager usedRectForTextContainer:textContainer])); + + NSRange glyphRange = [layoutManager glyphRangeForBoundingRect:CGRectMake(0,0,textContainer.size.width, textContainer.size.height) inTextContainer:textContainer]; + LOG(@"boundingRect: %@", NSStringFromCGRect([layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer])); + + [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; + [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; + + if (isScaled) { + // put the non-scaled version back + [scaledTextStorage removeLayoutManager:layoutManager]; + [textStorage addLayoutManager:layoutManager]; + } + }]; + } UIGraphicsPopContext(); CGContextRestoreGState(context); diff --git a/AsyncDisplayKitTests/ASSnapshotTestCase.h b/AsyncDisplayKitTests/ASSnapshotTestCase.h index 977e267d17..c89fc79af4 100644 --- a/AsyncDisplayKitTests/ASSnapshotTestCase.h +++ b/AsyncDisplayKitTests/ASSnapshotTestCase.h @@ -29,6 +29,9 @@ NSOrderedSet *ASSnapshotTestCaseDefaultSuffixes(void); #define ASSnapshotVerifyLayer(layer__, identifier__) \ FBSnapshotVerifyLayerWithOptions(layer__, identifier__, ASSnapshotTestCaseDefaultSuffixes(), 0); +#define ASSnapshotVerifyView(view__, identifier__) \ + FBSnapshotVerifyViewWithOptions(view__, identifier__, ASSnapshotTestCaseDefaultSuffixes(), 0); + @interface ASSnapshotTestCase : FBSnapshotTestCase /** diff --git a/AsyncDisplayKitTests/ASTextNodePerformanceTests.m b/AsyncDisplayKitTests/ASTextNodePerformanceTests.m index 9ed8f1ea0c..d3a0fa50e8 100644 --- a/AsyncDisplayKitTests/ASTextNodePerformanceTests.m +++ b/AsyncDisplayKitTests/ASTextNodePerformanceTests.m @@ -108,7 +108,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; ctx.results[kTestCaseASDK].userInfo[@"size"] = NSStringFromCGSize(asdkSize); ASXCTAssertEqualSizes(uiKitSize, asdkSize); - ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseASDK, 0.2, 0.5); + ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseASDK, 0.5, 0.9); } - (void)testPerformance_OneParagraphLatinWithTruncation diff --git a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m index 895f1841af..08b953374d 100644 --- a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m +++ b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m @@ -72,7 +72,24 @@ textNode.highlightRange = NSMakeRange(0, textNode.attributedText.length); [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:textNode]; - ASSnapshotVerifyLayer(backgroundView.layer, nil); + ASSnapshotVerifyView(backgroundView, nil); +} + +- (void)testThatFastPathTruncationWorks +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important" attributes:@{ NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont italicSystemFontOfSize:24] }]; + [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50))]; + ASSnapshotVerifyNode(textNode, nil); +} + +- (void)testThatSlowPathTruncationWorks +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important" attributes:@{ NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont italicSystemFontOfSize:24] }]; + [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50))]; + textNode.exclusionPaths = @[ [UIBezierPath bezierPath] ]; + ASSnapshotVerifyNode(textNode, nil); } @end diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatFastPathTruncationWorks@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d9f01fce31cece0c0d98b6dbb7ad2361a1e31499 GIT binary patch literal 4025 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 literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..dcae0039092c3ce3d71e5010f4d64442ad92bf72 GIT binary patch literal 4018 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 N0H$rMRiW$|M_TI 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 YrfUp-(iti}{6bhNN*apQ@|I!$1K%os`2YX_ literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatSlowPathTruncationWorks@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bf816c2ed4b7818599b00b5f06df29c7f770d53e GIT binary patch literal 3974 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 Z3yT^ndU;-(+dqg%Ps>QN`mr