How do I eliminate leading whitespace when computing word boundaries using boundingRectForGlyphRange in NSLayoutManager

爷,独闯天下 提交于 2019-12-11 02:08:02

问题


I am decomposing a multiline string into word boundaries on iOS. My solution centers around the boundingRectForGlyphRange method of NSLayoutManager. It ALMOST works, except that the rect for each word is a few pixels off to the right. In other words NSLayoutManager seems to be adding a leading space / indent on each line and I cannot find any way to override this behavior.

I tried using NSLayoutManager.usesFontLeading as well as NSParagraphStyle.headIndent but without any results:

 NSLayoutManager* layout = [NSLayoutManager new];
layout.usesFontLeading = NO;
NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.headIndent = paragraphStyle.firstLineHeadIndent = 0;
NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:self attributes:@{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
layout.textStorage = textStorage;
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:size];
[layout addTextContainer:textContainer];

// compute bounding rect for each word range
for (NSValue* wordRangeValue in wordRanges)
{
    NSRange wordRange = [wordRangeValue rangeValue];
    NSRange wordGlyphRange = [layout glyphRangeForCharacterRange:wordRange actualCharacterRange:NULL];
    CGRect wordBounds = [layout boundingRectForGlyphRange:wordGlyphRange inTextContainer:textContainer];
}

Screenshot: the gray rectangles represent label bounds, red rectangles represent text rect for each label and computed word boundaries from the [boundingRectForGlyphRange:] method above. Notice that the computed word boundaries are off by a few pixels.

I am also open to other methods for computing word boundaries, but boundingRectForGlyphRange seems very convenient for my purpose.


回答1:


To ignore the left margin, use:

textContainer.lineFragmentPadding = 0;



回答2:


I was not able to force NSLayoutManager to omit the left margin. If anyone knows how to get NSLayoutManager to ignore the left margin, let me know.

My workaround was to use Core Text instead. This was MUCH more difficult and involved. My solution does not lend itself to pasting into a single code excerpt, but this should give you a good reference if you want to go the same route:

- (NSArray*) coreTextLayoutsForCharacterRanges:(NSArray*)ranges withFont:(UIFont *)font constrainedToSize:(CGSize)size{

// initialization: make frame setter
NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; // watch out - need to specify line wrapping to use multi line layout!
paragraphStyle.minimumLineHeight = font.lineHeight; // watch out - custom fonts do not compute properly without this!
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:self attributes:@{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFRange wholeString = CFRangeMake(0, self.length);
CGRect bounds = CGRectMake(0, 0, size.width, size.height);
CGMutablePathRef boundsPath = CGPathCreateMutable();
CGPathAddRect(boundsPath, NULL, bounds);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, wholeString, boundsPath, NULL);
CFRelease(boundsPath);

// extract lines
CFArrayRef lines = CTFrameGetLines(frame);
int lineCount = CFArrayGetCount(lines);
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);

NSMutableArray* lineLayouts = [NSMutableArray arrayWithCapacity:lineCount];
CGFloat h = size.height;
for (int i = 0; i < lineCount; ++i){
    CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
    CGPoint lineOrigin = lineOrigins[i];  // in Core Graphics coordinates! let's convert.
    lineOrigin.y = h - lineOrigin.y;
    TextLayout* lineLayout = [[CTLineLayout alloc] initWithString:self line:line lineOrigin:lineOrigin];
    [lineLayouts addObject:lineLayout];
}

// got line layouts. now we iterate through the word ranges to find the appropriate line for each word and compute its layout using the corresponding CTLine.

Another important part is how to get the bounding rect of a word in a line by using CTLine. I factored this into a CTLineLayout module, but the gist is this (the 'origin' variable refers to the line origin computed in the code sample above):

    CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat top = origin.y - ascent;
CGFloat bottom = origin.y + descent;
CGRect result = CGRectMake(origin.x, top, width, bottom - top); // frame of entire line (for now)
CGFloat left = CTLineGetOffsetForStringIndex(line, stringRange.location, NULL);
CGFloat right = CTLineGetOffsetForStringIndex(line, NSMaxRange(stringRange), NULL);
result.origin.x = left;
result.size.width = right - left; // frame of target word in UIKit coordinates

The above is a rough excerpt - I factored CTLine to compute the line's bounds once in the initializer, then compute only the left + right endpoints when getting the frame of a word.

Whew!



来源:https://stackoverflow.com/questions/21666258/how-do-i-eliminate-leading-whitespace-when-computing-word-boundaries-using-bound

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!