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 37dfc598..1bbdbbfd 100644 Binary files a/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetHighlight_ASTextNode2@2x.png and b/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextContainerInsetHighlight_ASTextNode2@2x.png differ