From 2c9e51e8f7d2b7c963be0200fce1e02de4f34f7f Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Fri, 12 Aug 2016 12:07:00 -0700 Subject: [PATCH] Add support for textContainerInset to ASTextNode (ala UITextView) (#2062) * Add support for textContainerInset to ASTextNode (ala UITextView) * Better comment, parens to increase readability. Thanks @schneider! * Add textContainerInset snapshot test. --- AsyncDisplayKit/ASTextNode+Beta.h | 8 +++ AsyncDisplayKit/ASTextNode.mm | 56 ++++++++++++++++-- .../ASTextNodeSnapshotTests.m | 35 +++++++++++ .../testTextContainerInset@2x.png | Bin 0 -> 2651 bytes 4 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 AsyncDisplayKitTests/ASTextNodeSnapshotTests.m create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png 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 0000000000000000000000000000000000000000..7e6cac14b804f09ecdece6aa2d7e8e59d44ecd48 GIT binary patch literal 2651 zcmai$cT^IL*T)$`xwFhs4&2I9IdbMU4awXqEl^9%5v78ODJ~LI6U|wy_k2mS1v_y70a^Sz&Y@1OUad(TaFutNy)o!|oi0D?$sOQ!?k4s`A? z^dS0l6ATXk#5y4?0I&L`<_`+q5Ni)C03fFHS3snb;>tl&D$vs2<$$0A{Wa+Wss0}i zavlRJPWSo&AxKMe7d&v$19jilQB=m?LShxR>Rm2^?EOq!b#9Rpw)jkiv^C$2W<0L-zjYxEM%%%$nn0ZjR%(5R?CS-b_bB-(CwHZ^XK%JBLI=@-Gmd zILsh=X89Umcbd;PUw_I9fLuJHSNu54;$xV0o`f!(+UQ~M_%?qjd_RK~?fswEYum!y zFjc9fG31jBMojK6O{4gO-MP|WcH`6L1`XAF}**HB171Xucu0S z%f1%!mfDZkb^_MDknKbpNTzgoG7g8xGyw_#Zon!(&?(;`bpheDltvoEgL4$eX4)(J zjSHI5&M*vM%;BYAb`8(7mDCJYxY%#A6+J+DPT|_@1nV^esqzN7-Nz{6uMk@yfEoH% zEvph`g|B9`g(YsWFrN&14_20^bzf8r|s(V zmm?j*`95YhehKi*F~uhu3{-Hh?9qBpO({}|iMYCHrq5xVDI~p^7lFsijVSwkUd8*8 zd=1WZg0*zVf3RvboM;=uakBKt2lJ0!D#U0t-3BbBZuYH%VT8aL*EMl)?rXPSUpU96 zWC0Db1k+Dp5<_%X^4#6-;hz7tZeQ{oL~QiOf$fx4E|7aRY`v8<_!yxd(q+%ac`_6j zTVsQEC$MTwKNC+A8o)_Fv227%;xS#Zub?)bsmseJdXrR$%=v}jH61|o%nd+SyFew- zB9&+Q4GJxEq}nyN+KGu%7|TSB$=GUMpg+s<8~`0qB3M;{-`Y`9*aL@MPrCf$h=ywx~- z$q5=hv@%+b&w+k+x{+Vd+s|rK_tbZN%Np|G8U`HP%Bk`?%Qi z?Farzy_I_uu zq!6_vXq$YsPXAcG)q@i(0i0$`|5q;t-s|jg?(D7H7QNAv<`Vctspz& zk-UCd36*h&<{=uAQk`q)6(Oec;@v`TwEckaC9qmVkQd-#-}%7E2*fjgwq1C9u782a z0v;oYwlk)V-}Y`pnZ?jn%XtdQs&o3swm>9fh5%GNVce{umv2RF3(AgfRF9ihT=zTH zqm`W-1WZ9TdznSz9rNlo z`N8zMwkrxwf`!%1lzti)7+b)MmN*#WD_n;^g!FV^TLB$0{TQg{gdz?7@H4lx*osIdL0c#oqhCSjQVkNoJME$`l1xNghB%@=gZ zpNskwlA+c%__c{Am{`PKR4eJgJ@5VpPvIX@xughJJ*8gFk~f^c1K~Ec%bj=2k?BFHo&ar5F0dQ=wO`Z~wW^haPih1>q&Udzpj3l?blvk&L55i?|<^vmVi!B-+6Mk)eGu2kpq{v8cd>N zO-%NGuoc>i*fF2mc}+;to#bgCmE$&g_>BtI%bkoL3s?t5Pr5L~=-R|BUl2t>LKB7esY(B*7J zZzjEs60}5ge4~J+JCV1_vc6RnW-A9Z7;5VAzW|TwuDh2M^LfVk8d+lxc7yVDwM*-a zRXKCy44Z6cIZyl)Qex=BZwwTzI$9cj5x2=MdU8110dq}#;L<#)o=BA_q3iHT3B193 zbn`8d$dljPub$S(2f9YTf$yc(_Hr-284~F8p59ox+K|H~V`9eJd#DtKy|><48OG%7 z6-Urcy_;)7aQyW9VXu^q24PgR`w&Z@Ahok0vIF($YNs;o->IAeAVGFJ%|!&KF5wWi z*h}D*9EkllJz?8hIRJbp;bOIbi)5vigI`InSx$U3lnW-@N=ys%;tc$K{*YF7mai