diff --git a/Sources/MPILabel.h b/Sources/MPILabel.h index b40b84a..2c0f102 100644 --- a/Sources/MPILabel.h +++ b/Sources/MPILabel.h @@ -244,6 +244,13 @@ Whether or not the text is truncated. It's expensive if text not rendered. */ @property (nonatomic) BOOL fadeOnAsynchronouslyDisplay; +/** + Additional interactive area when to didInteractWithLink + + The default value is UIEdgeInsetsZero. negative values ​​are not allowed. + */ +@property (nonatomic) UIEdgeInsets interactionLinkPointExpansionInsets; + /// To show menu by selectedRange. - (void)showMenu; diff --git a/Sources/MPILabel.m b/Sources/MPILabel.m index 96582e5..4900352 100644 --- a/Sources/MPILabel.m +++ b/Sources/MPILabel.m @@ -695,11 +695,16 @@ - (NSRange)linkRangeAtPoint:(CGPoint)point inTruncation:(nullable BOOL *)inTrunc point = [self convertPointToTextKit:point forBounds:self.bounds textSize:renderer.size]; + UIEdgeInsets expansion = self.interactionLinkPointExpansionInsets; NSRange linkRange; #ifdef DEBUG MPITextLink *link = #endif - [renderer attribute:MPITextLinkAttributeName atPoint:point effectiveRange:&linkRange inTruncation:inTruncation]; + [renderer attribute:MPITextLinkAttributeName + atPoint:point + expansion:expansion + effectiveRange:&linkRange + inTruncation:inTruncation]; #ifdef DEBUG if (linkRange.location != NSNotFound) { NSAssert([link isKindOfClass:MPITextLink.class], @"The value for MPITextLinkAttributeName must be of type MPITextLink."); diff --git a/Sources/MPITextRenderer.h b/Sources/MPITextRenderer.h index 7bc8ab4..7266b26 100644 --- a/Sources/MPITextRenderer.h +++ b/Sources/MPITextRenderer.h @@ -67,12 +67,14 @@ NS_ASSUME_NONNULL_BEGIN @param name The name of an attribute. @param point The index at which to test for attributeName. + @param expansion The point expands outwards to form a rectangle. @param effectiveRange If the named attribute does not exist at index, the range is (NSNotFound, 0). @param inTruncation Indicates the attribute is in truncation. @return Returns The value for the attribute named attributeName of the character at index, or nil if there is no such attribute. */ - (nullable id)attribute:(NSAttributedStringKey)name atPoint:(CGPoint)point + expansion:(UIEdgeInsets)expansion effectiveRange:(nullable NSRangePointer)effectiveRange inTruncation:(nullable BOOL *)inTruncation; diff --git a/Sources/MPITextRenderer.m b/Sources/MPITextRenderer.m index cdee291..e3aaab9 100644 --- a/Sources/MPITextRenderer.m +++ b/Sources/MPITextRenderer.m @@ -241,45 +241,83 @@ @implementation MPITextRenderer (MPITextKitExtendedRenderer) - (nullable id)attribute:(NSAttributedStringKey)name atPoint:(CGPoint)point + expansion:(UIEdgeInsets)expansion effectiveRange:(nullable NSRangePointer)effectiveRange - inTruncation:(BOOL *)pInTruncation { + inTruncation:(nullable BOOL *)inTruncation { __block id value = nil; - __block NSRange attributeRange = NSMakeRange(NSNotFound, 0); - __block BOOL inTruncation; + __block NSRange pEffectiveRange = NSMakeRange(NSNotFound, 0); + __block BOOL pInTruncation = NO; NSValue *truncationCharacterRange = self.truncationInfo.truncationCharacterRange; + CGRect boundingRect = CGRectMake(point.x - expansion.left, + point.y - expansion.top, + expansion.left + expansion.right, + expansion.top + expansion.bottom); [self.context performBlockWithLockedTextKitComponents:^(MPITextLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { - // Find the range. - NSRange visibleGlyphsRange = [layoutManager glyphRangeForBoundingRect:(CGRect){ .size = textContainer.size } - inTextContainer:textContainer]; + NSRange visibleGlyphsRange = [layoutManager glyphRangeForBoundingRect:boundingRect inTextContainer:textContainer]; NSRange visibleCharactersRange = [layoutManager characterRangeForGlyphRange:visibleGlyphsRange actualGlyphRange:NULL]; - NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:point inTextContainer:textContainer]; - if (glyphIndex != NSNotFound) { - CGRect glyphRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1) inTextContainer:textContainer]; - if (CGRectContainsPoint(glyphRect, point)) { - NSUInteger characterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex]; - value = [textStorage attribute:name atIndex:characterIndex longestEffectiveRange:&attributeRange inRange:visibleCharactersRange]; - if (!value) { - attributeRange = NSMakeRange(NSNotFound, 0); + + __block NSUInteger characterIndex = NSNotFound; + if (visibleCharactersRange.length > 0) { + __block CGFloat minDistance = CGFLOAT_MAX; + [textStorage enumerateAttribute:name + inRange:visibleCharactersRange + options:0 + usingBlock:^(id _Nullable attrValue, NSRange attrRange, BOOL *attrStop) { + if (attrValue == nil) { + return; + } + NSRange attrGlyphRange = [layoutManager glyphRangeForCharacterRange:attrRange actualCharacterRange:NULL]; + [layoutManager enumerateEnclosingRectsForGlyphRange:attrGlyphRange + withinSelectedGlyphRange:NSMakeRange(NSNotFound, 0) + inTextContainer:textContainer + usingBlock:^(CGRect attRect, BOOL *innerStop) { + if (!CGRectIntersectsRect(attRect, boundingRect)) { + return; + } + + CGFloat dx = MAX(MAX(CGRectGetMinX(attRect) - point.x, 0), point.x - CGRectGetMaxX(attRect)); + CGFloat dy = MAX(MAX(CGRectGetMinY(attRect) - point.y, 0), point.y - CGRectGetMaxY(attRect)); + CGFloat distance = sqrt(dx * dx + dy * dy); + + if (distance < minDistance) { + minDistance = distance; + characterIndex = attrRange.location; + } + }]; + }]; + } else { + NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:point inTextContainer:textContainer]; + if (glyphIndex != NSNotFound) { + CGRect glyphRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1) inTextContainer:textContainer]; + if (CGRectContainsPoint(glyphRect, point)) { + characterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex]; } } } + if (characterIndex != NSNotFound) { + NSRange fullGlyphsRange = [layoutManager glyphRangeForBoundingRect:(CGRect){ .size = textContainer.size } inTextContainer:textContainer]; + NSRange fullCharactersRange = [layoutManager characterRangeForGlyphRange:fullGlyphsRange actualGlyphRange:NULL]; + value = [textStorage attribute:name atIndex:characterIndex longestEffectiveRange:&pEffectiveRange inRange:fullCharactersRange]; + } + if (!value) { + pEffectiveRange = NSMakeRange(NSNotFound, 0); + } // Check that the range is in truncation. - if (truncationCharacterRange && - NSLocationInRange(attributeRange.location, truncationCharacterRange.rangeValue)) { - inTruncation = YES; - attributeRange = NSMakeRange(attributeRange.location - truncationCharacterRange.rangeValue.location, attributeRange.length); + if (truncationCharacterRange && NSLocationInRange(pEffectiveRange.location, truncationCharacterRange.rangeValue)) { + pInTruncation = YES; + pEffectiveRange = NSMakeRange(pEffectiveRange.location - truncationCharacterRange.rangeValue.location, pEffectiveRange.length); } else { - inTruncation = NO; + pInTruncation = NO; } }]; if (effectiveRange) { - *effectiveRange = attributeRange; + *effectiveRange = pEffectiveRange; } - if (pInTruncation) { - *pInTruncation = inTruncation; + if (inTruncation) { + *inTruncation = pInTruncation; } return value; @@ -288,7 +326,7 @@ - (nullable id)attribute:(NSAttributedStringKey)name - (NSUInteger)characterIndexForPoint:(CGPoint)point { __block NSUInteger characterIndex = NSNotFound; [self.context performBlockWithLockedTextKitComponents:^(MPITextLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { - NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:point inTextContainer:textContainer]; + NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:point inTextContainer:textContainer]; if (glyphIndex != NSNotFound) { characterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex]; }