Skip to content

Commit

Permalink
When determining the bounds for links in the attributedText, keep tra…
Browse files Browse the repository at this point in the history
…ck of links in previous lines if they don't have any linked text after them in their respective lines. Use CGRectUnion (the smallest rectangle that contains the two source rectangles) to combine the bounds of a link in the current line with the combined bounds from previous links.

This prevents duplicate announcement of the same accessibility element during VoiceOver for attributedLabel text that contains links spanning multiple lines.

PiperOrigin-RevId: 529103370
  • Loading branch information
Nobody authored and material-automation committed May 3, 2023
1 parent e660d27 commit 390b1ce
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 2 deletions.
6 changes: 5 additions & 1 deletion src/attributedlabel/src/NIAttributedLabel.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ extern NSString* const NIAttributedLabelLinkAttributeName; // Value is an NSText
* the correct dimensions of the attributed label before setting the frame.
*
* NIAttributedLabel implements the UIAccessibilityContainer methods to expose each link as an
* accessibility item.
* accessibility item. A note on performance, @c NIAttributedLabel generates these elements with an O(l*a)
* complexity where l is the number of links in the text and a is the number of segments of differently
* attributed text. Text arrangements where l~a have a complexity approaching O(a^2), while cases where
* l<<a have a complexity that approaches O(a).
*
* @ingroup NimbusAttributedLabel
*/
Expand Down Expand Up @@ -139,6 +142,7 @@ extern NSString* const NIAttributedLabelLinkAttributeName; // Value is an NSText
@property (nonatomic) BOOL shouldSortLinksLast DEPRECATED_MSG_ATTRIBUTE("Use linkOrdering instead. Besides sorting links as first or last accessible elements, we are introducing a new way which sorts links in their original order and breaks the text into fragments when necessary."); // Sort the links in the text as the last elements in accessible elements. Default: NO

@property (nonatomic) NILinkOrdering linkOrdering; // Define how to sort links in the text. Default: NILinkOrderFirst
@property(nonatomic) BOOL shouldMergeMultilineLinks;

/**
* Configures if the label's accessibility elements should remember their last valid accessibility
Expand Down
64 changes: 63 additions & 1 deletion src/attributedlabel/src/NIAttributedLabel.m
Original file line number Diff line number Diff line change
Expand Up @@ -1042,7 +1042,67 @@ - (NSArray *)_rectsForRange:(NSRange)range {
[rects addObject:[NSValue valueWithCGRect:rect]];
}
}
return [rects copy];
}

- (NSArray *)_multilineRectsForRange:(NSRange)range {
CFArrayRef lines = CTFrameGetLines(self.textFrame);
if (nil == lines) {
return nil;
}
CFIndex count = CFArrayGetCount(lines);
CGPoint lineOrigins[count];
CTFrameGetLineOrigins(self.textFrame, CFRangeMake(0, 0), lineOrigins);

NSMutableArray *rects = [[NSMutableArray alloc] initWithCapacity:count];

NSRange runningRange = NSMakeRange(0,0);
CGRect runningRect = CGRectZero;

for (CFIndex lineIndex = 0; lineIndex < count; lineIndex++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
CGAffineTransform transform = [self _transformForCoreText];
CGFloat verticalOffset = [self _verticalOffsetForBounds:self.bounds];

CFArrayRef runs = CTLineGetGlyphRuns(line);
CFIndex runCount = CFArrayGetCount(runs);
for (CFIndex runIndex = 0; runIndex < runCount; runIndex++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, runIndex);

CFRange stringRunRange = CTRunGetStringRange(run);
NSRange lineRunRange = NSMakeRange(stringRunRange.location, stringRunRange.length);
NSRange intersectedRunRange = NSIntersectionRange(lineRunRange, range);

if (intersectedRunRange.length == 0) {
// This run is not attributed since this run does not intersect the range, add the rect for
// runningRange to rects and clear runningRange.
if (runningRange.length > 0) {
[rects addObject:[NSValue valueWithCGRect:runningRect]];
runningRange = NSMakeRange(0,0);
runningRect = CGRectZero;
}
continue;
}
// Run must be attributed: update the attributed runningRange.
CGRect intersectedRect = [self _rectForRange:intersectedRunRange
inLine:line
lineOrigin:lineOrigins[lineIndex]];
intersectedRect = CGRectApplyAffineTransform(intersectedRect, transform);
intersectedRect = CGRectOffset(intersectedRect, 0, verticalOffset);
if (runningRange.length == 0) {
runningRange = intersectedRunRange;
runningRect = intersectedRect;
} else {
runningRange = NSUnionRange(runningRange, intersectedRunRange);
runningRect = CGRectUnion(runningRect, intersectedRect);
}
}
}

// Add any cached runningRange to the rects.
if (runningRange.length > 0) {
[rects addObject:[NSValue valueWithCGRect:runningRect]];
}
return [rects copy];
}

Expand Down Expand Up @@ -1839,7 +1899,9 @@ - (NSArray *)accessibleElements {
// TODO(kaikaiz): remove the first condition when shouldSortLinksLast is fully deprecated.
if ((_shouldSortLinksLast || (_linkOrdering != NILinkOrderingOriginal)) && !entireLabelIsOneLink) {
for (NSTextCheckingResult *result in allLinks) {
NSArray *rectsForLink = [self _rectsForRange:result.range];
NSArray *rectsForLink = _shouldMergeMultilineLinks
? [self _multilineRectsForRange:result.range]
: [self _rectsForRange:result.range];
if (!NIIsArrayWithObjects(rectsForLink)) {
continue;
}
Expand Down

0 comments on commit 390b1ce

Please sign in to comment.