diff --git a/AsyncDisplayKit/ASTextNode+Beta.h b/AsyncDisplayKit/ASTextNode+Beta.h index 899b7d2175..1e6dc38765 100644 --- a/AsyncDisplayKit/ASTextNode+Beta.h +++ b/AsyncDisplayKit/ASTextNode+Beta.h @@ -31,6 +31,14 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, copy) NSTextStorage * (^textStorageCreationBlock)(NSAttributedString *_Nullable attributedString); +/** + @abstract Text margins for text laid out in the text node. + @discussion defaults to UIEdgeInsetsZero. + This property can be useful for handling text which does not fit within the view by default. An example: like UILabel, + ASTextNode will clip the left and right of the string "judar" if it's rendered in an italicised font. + */ +@property (nonatomic, assign) UIEdgeInsets textContainerInset; + @end NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index e4edbbd99d..b658be8a4e 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -48,6 +48,8 @@ struct ASTextNodeDrawParameter { UIColor *_cachedShadowUIColor; CGFloat _shadowOpacity; CGFloat _shadowRadius; + + UIEdgeInsets _textContainerInset; NSArray *_exclusionPaths; @@ -213,7 +215,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; ASDN::MutexLocker l(__instanceLock__); if (_renderer == nil) { - CGSize constrainedSize = _constrainedSize.width != -INFINITY ? _constrainedSize : bounds.size; + CGSize constrainedSize; + if (_constrainedSize.width != -INFINITY) { + constrainedSize = _constrainedSize; + } else { + constrainedSize = bounds.size; + constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); + constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); + } + _renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:[self _rendererAttributes] constrainedSize:constrainedSize]; } @@ -279,6 +289,24 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - Layout and Sizing +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + ASDN::MutexLocker l(__instanceLock__); + + BOOL needsUpdate = !UIEdgeInsetsEqualToEdgeInsets(textContainerInset, _textContainerInset); + if (needsUpdate) { + _textContainerInset = textContainerInset; + [self invalidateCalculatedLayout]; + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)textContainerInset +{ + ASDN::MutexLocker l(__instanceLock__); + return _textContainerInset; +} + - (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize { ASDN::MutexLocker l(__instanceLock__); @@ -291,6 +319,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // a new one. However, there are common cases where the constrained size doesn't need to be the same as calculated. CGSize rendererConstrainedSize = _renderer.constrainedSize; + //inset bounds + boundsSize.width -= _textContainerInset.left + _textContainerInset.right; + boundsSize.height -= _textContainerInset.top + _textContainerInset.bottom; + if (CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) { return NO; } else { @@ -321,9 +353,14 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; if (layout != nil) { ASDN::MutexLocker l(__instanceLock__); - if (CGSizeEqualToSize(_constrainedSize, layout.size) == NO) { - _constrainedSize = layout.size; - _renderer.constrainedSize = layout.size; + CGSize layoutSize = layout.size; + //Apply textContainerInset + layoutSize.width -= (_textContainerInset.left + _textContainerInset.right); + layoutSize.height -= (_textContainerInset.top + _textContainerInset.bottom); + + if (CGSizeEqualToSize(_constrainedSize, layoutSize) == NO) { + _constrainedSize = layoutSize; + _renderer.constrainedSize = layoutSize; } } } @@ -335,6 +372,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; ASDN::MutexLocker l(__instanceLock__); + //remove textContainerInset + constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); + constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); + _constrainedSize = constrainedSize; // Instead of invalidating the renderer, in case this is a new call with a different constrained size, @@ -353,6 +394,11 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; self.descender *= _renderer.currentScaleFactor; } } + + //add textContainerInset + size.width += (_textContainerInset.left + _textContainerInset.right); + size.height += (_textContainerInset.top + _textContainerInset.bottom); + return size; } @@ -466,6 +512,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; CGContextSaveGState(context); + CGContextTranslateCTM(context, _textContainerInset.left, _textContainerInset.top); + ASTextKitRenderer *renderer = [self _rendererWithBounds:drawParameterBounds]; UIEdgeInsets shadowPadding = [self shadowPaddingWithRenderer:renderer]; CGPoint boundsOrigin = drawParameterBounds.origin; diff --git a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m new file mode 100644 index 0000000000..78b420bfc5 --- /dev/null +++ b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m @@ -0,0 +1,35 @@ +// +// ASTextNodeSnapshotTests.m +// AsyncDisplayKit +// +// Created by Garrett Moon on 8/12/16. +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "ASSnapshotTestCase.h" + +#import + +@interface ASTextNodeSnapshotTests : ASSnapshotTestCase + +@end + +@implementation ASTextNodeSnapshotTests + +- (void)testTextContainerInset +{ + // trivial test case to ensure ASSnapshotTestCase works + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar" + attributes:@{NSFontAttributeName : [UIFont italicSystemFontOfSize:24]}]; + [textNode measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + textNode.frame = CGRectMake(0, 0, textNode.calculatedSize.width, textNode.calculatedSize.height); + textNode.textContainerInset = UIEdgeInsetsMake(0, 2, 0, 2); + + ASSnapshotVerifyNode(textNode, nil); +} + +@end diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png new file mode 100644 index 0000000000..7e6cac14b8 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png differ