From ab8ea069ea298bbbaf4ef13bd41d64d957027cc6 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 3 Jun 2019 08:57:31 -0700 Subject: [PATCH] Add support for basic ASTextNode2 link highlighting (#1518) * Add support for basic ASTextNode2 link highlighting * Add approach to fix line highlighting * Enable proper highlighting for first row * Fix snapshot highlighting test for ASTextNode2 * Update screenshot again * Address comments --- Source/ASTextNode.mm | 2 +- Source/ASTextNode2.mm | 113 +++++++++++++++++- .../TextExperiment/Component/ASTextLayout.mm | 4 +- ...ContainerInsetHighlight_ASTextNode2@2x.png | Bin 3926 -> 4607 bytes 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 63661944..d0d81640 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -766,6 +766,7 @@ static NSArray *DefaultLinkAttributeNames() { - (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *)highlightedAttributeName value:(id)highlightedAttributeValue animated:(BOOL)animated { ASDisplayNodeAssertMainThread(); + ASLockScopeSelf(); _highlightedLinkAttributeName = highlightedAttributeName; _highlightedLinkAttributeValue = highlightedAttributeValue; @@ -825,7 +826,6 @@ static NSArray *DefaultLinkAttributeNames() { } if (highlightTargetLayer != nil) { - ASLockScopeSelf(); ASTextKitRenderer *renderer = [self _locked_renderer]; NSArray *highlightRects = [renderer rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index a8f7f248..a6e0bb07 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -137,6 +137,8 @@ static NS_RETURNS_RETAINED ASTextLayout *ASTextNodeCompatibleLayoutWithContainer return layout; } +static const NSTimeInterval ASTextNodeHighlightFadeOutDuration = 0.15; +static const NSTimeInterval ASTextNodeHighlightFadeInDuration = 0.1; static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; @@ -778,17 +780,114 @@ static NSArray *DefaultLinkAttributeNames() { - (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *)highlightedAttributeName value:(id)highlightedAttributeValue animated:(BOOL)animated { + ASDisplayNodeAssertMainThread(); ASLockScopeSelf(); // Protect usage of _highlight* ivars. // Set these so that link tapping works. _highlightedLinkAttributeName = highlightedAttributeName; _highlightedLinkAttributeValue = highlightedAttributeValue; - _highlightRange = highlightRange; - AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); - // Much of the code from original ASTextNode is probably usable here. + if (!NSEqualRanges(highlightRange, _highlightRange) && ((0 != highlightRange.length) || (0 != _highlightRange.length))) { - return; + _highlightRange = highlightRange; + + if (_activeHighlightLayer) { + if (animated) { + __weak CALayer *weakHighlightLayer = _activeHighlightLayer; + _activeHighlightLayer = nil; + + weakHighlightLayer.opacity = 0.0; + + CFTimeInterval beginTime = CACurrentMediaTime(); + CABasicAnimation *possibleFadeIn = (CABasicAnimation *)[weakHighlightLayer animationForKey:@"opacity"]; + if (possibleFadeIn) { + // Calculate when we should begin fading out based on the end of the fade in animation, + // Also check to make sure that the new begin time hasn't already passed + CGFloat newBeginTime = (possibleFadeIn.beginTime + possibleFadeIn.duration); + if (newBeginTime > beginTime) { + beginTime = newBeginTime; + } + } + + CABasicAnimation *fadeOut = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fadeOut.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + fadeOut.fromValue = possibleFadeIn.toValue ? : @(((CALayer *)weakHighlightLayer.presentationLayer).opacity); + fadeOut.toValue = @0.0; + fadeOut.fillMode = kCAFillModeBoth; + fadeOut.duration = ASTextNodeHighlightFadeOutDuration; + fadeOut.beginTime = beginTime; + + dispatch_block_t prev = [CATransaction completionBlock]; + [CATransaction setCompletionBlock:^{ + [weakHighlightLayer removeFromSuperlayer]; + }]; + + [weakHighlightLayer addAnimation:fadeOut forKey:fadeOut.keyPath]; + + [CATransaction setCompletionBlock:prev]; + + } else { + [_activeHighlightLayer removeFromSuperlayer]; + _activeHighlightLayer = nil; + } + } + if (0 != highlightRange.length) { + // Find layer in hierarchy that allows us to draw highlighting on. + CALayer *highlightTargetLayer = self.layer; + while (highlightTargetLayer != nil) { + if (highlightTargetLayer.as_allowsHighlightDrawing) { + break; + } + highlightTargetLayer = highlightTargetLayer.superlayer; + } + + if (highlightTargetLayer != nil) { + // TODO: The copy and application of size shouldn't be required, but it is currently. + // See discussion in https://github.com/TextureGroup/Texture/pull/396 + ASTextContainer *textContainerCopy = [_textContainer copy]; + textContainerCopy.size = self.calculatedSize; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(textContainerCopy, _attributedText); + + NSArray *highlightRects = [layout selectionRectsWithoutStartAndEndForRange:[ASTextRange rangeWithRange:highlightRange]]; + NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; + + CALayer *layer = self.layer; + UIEdgeInsets shadowPadding = self.shadowPadding; + for (ASTextSelectionRect *rectValue in highlightRects) { + // Adjust shadow padding + CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.rect, shadowPadding); + CGRect highlightedRect = [layer convertRect:rendererRect toLayer:highlightTargetLayer]; + + // We set our overlay layer's frame to the bounds of the highlight target layer. + // Offset highlight rects to avoid double-counting target layer's bounds.origin. + highlightedRect.origin.x -= highlightTargetLayer.bounds.origin.x; + highlightedRect.origin.y -= highlightTargetLayer.bounds.origin.y; + [converted addObject:[NSValue valueWithCGRect:highlightedRect]]; + } + + ASHighlightOverlayLayer *overlayLayer = [[ASHighlightOverlayLayer alloc] initWithRects:converted]; + overlayLayer.highlightColor = [[self class] _highlightColorForStyle:self.highlightStyle]; + overlayLayer.frame = highlightTargetLayer.bounds; + overlayLayer.masksToBounds = NO; + overlayLayer.opacity = [[self class] _highlightOpacityForStyle:self.highlightStyle]; + [highlightTargetLayer addSublayer:overlayLayer]; + + if (animated) { + CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fadeIn.fromValue = @0.0; + fadeIn.toValue = @(overlayLayer.opacity); + fadeIn.duration = ASTextNodeHighlightFadeInDuration; + fadeIn.beginTime = CACurrentMediaTime(); + + [overlayLayer addAnimation:fadeIn forKey:fadeIn.keyPath]; + } + + [overlayLayer setNeedsDisplay]; + + _activeHighlightLayer = overlayLayer; + } + } + } } - (void)_clearHighlightIfNecessary @@ -812,6 +911,12 @@ static NSArray *DefaultLinkAttributeNames() { #pragma mark - Text rects +static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UIEdgeInsets shadowPadding) { + rendererRect.origin.x -= shadowPadding.left; + rendererRect.origin.y -= shadowPadding.top; + return rendererRect; +} + - (NSArray *)rectsForTextRange:(NSRange)textRange { AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.mm b/Source/Private/TextExperiment/Component/ASTextLayout.mm index 7549649f..2e61f819 100644 --- a/Source/Private/TextExperiment/Component/ASTextLayout.mm +++ b/Source/Private/TextExperiment/Component/ASTextLayout.mm @@ -2146,7 +2146,9 @@ dispatch_semaphore_signal(_lock); if (isVertical) { topRect.rect = CGRectMake(startLine.left, topOffset, startLine.width, (_container.path ? startLine.bottom : _container.size.height - _container.insets.bottom) - topOffset); } else { - topRect.rect = CGRectMake(topOffset, startLine.top, (_container.path ? startLine.right : _container.size.width - _container.insets.right) - topOffset, startLine.height); + // TODO: Fixes highlighting first row only to the end of the text and not highlight + // the while line to the end. Needs to brought over to multiline support + topRect.rect = CGRectMake(topOffset, startLine.top, (_container.path ? startLine.right : _container.size.width - _container.insets.right) - topOffset - (_container.size.width - _container.insets.right - startLine.right), startLine.height); } } [rects addObject:topRect]; diff --git a/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetHighlight_ASTextNode2@2x.png b/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetHighlight_ASTextNode2@2x.png index 37dfc5986d5df25f8a4be4310eb36f51549512d3..1bbdbbfdb304316407e00fb33be025887b87199c 100644 GIT binary patch literal 4607 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
vrcp<-Fz) zAh5*(PsjRYvHElkd%jayH}~pm=f=(aWLoAk^g0XiD-kD0jA`pAteeVx%sGuEKwz5$ zE{lC?g)94~w)}R#_Gz*XF@Ex(2THKKpaVevW(^;Z!GBEDTRHs|GQhw~zav{ef0rgI;1K4SVe>)qe6-bvB_O=W!M@Mpx{h#MnLj96^D;^LNwXCrE#j&pv#jyNu2 zS({o|TgZLc+1w{UU_%0@#=d_Q%o?3r0aY|urCHEyqbDIExO$rH3}p#unC=a_>2oTLcKSAaH+eBvBUD&qpNoL1q4R9rItT zjD5Xh?AY#4Dr-N|v&h)O+EVU4$8w7RfmQ_ay=qxpuN;uv?if?P2IwowM=JX~Ghm+~ zLtd+_-B+~77S@(>AL3yQ0RkHq$WL}{Xhq=cNF;wUzgU>Q9FW}MV$MHtoxjssYdfQ}{B)c9Wr15q z%L2Wae}4U4&%$F0Yiqgp9Lp^N1X>a}7|E1{>8hRtFQ|+^)H6Q+u|T+<2LJ&7|GRBgjZz^S;mXedaswe7{rw-sgSJ?SFn#f3JBjvu2I9S_N*5ga4DowhG3kY;2l)OJ)2C zh4FuB&icxnF+h{BaAjfqM+4(q%Y7hCw+RqvP2k8#=;w%jaotr(^4)CgFY)}S z%J?I)@!dX`ROXE7x~Z`K?#cpV3Ttb*SB|BN0D;y7UWkOcg4aEsT}X06Vchn@xct_y zg?TsUgC;)TTv+?l%35OzYg(qI+$+b@MSy=mYXX->LR~@a9v>|vd4FNtWrcD1t#1qS zw&a5*J}>=8VMk@Hv4ypz+$+b@MSwtS0tZDxUqy69H9dY_Nbnd}{7S@(>uN+Gk0Rk-woEAyt1*%^-A1L&nG3P-pEX+-N-8<*} zp)?7rjK^&-&i!_04mcKGM`*J6-RN zNS{iW($>YK(i=;!&sc11VNJ`mlKX#*qv<9IGlIj<$FXQ=#*}Z>PCnkPz z%=;r-tJ~}CxPQ`&b1$4I$~4AixImb{)*_2?_b{#ee<-bg*C0c(z(xbK4SfIwpcA4EQRk^6t-f6DQkSa)kg z`sJ+j0snm8^gR9j>yC)@MTJzKiMR zi){%IAh7TPhsFuMkH~)uYx+#7Mt}eT0uu!8iaB{v{W|U)JYg+U2oNB!>IKdZf!`wX z!g|}PmjOV40D&0-=~w?#bS=FpwIyOtL|#7k5Fk(ySRZ|RBhpt_OU1M+ zuIZBi8zVFV1PBnAA&?4dS3GNvn`f+}n*ad<1pX74Ran!j*!?x}WdMI51PBl~U<8H= z>%KVQvk_CigMYx_I0yj(1f~ca9+R$*xF;h0Qq$!T>0e>>CRHG4voR000+S#CFBn*= Zz`uf1gU$+_SVsT=002ovPDHLkV1l3aspbFx