CTFramesetterSuggestFrameSizeWithConstraints() Core Text каждый раз возвращает неверный размер
Согласно документам, CTFramesetterSuggestFrameSizeWithConstraints ()
msgstr "определяет размер кадра, необходимый для диапазона строк".
К сожалению, размер, возвращаемый этой функцией, никогда не бывает точным. Вот что я делаю:
NSAttributedString *string = [[[NSAttributedString alloc] initWithString:@"lorem ipsum" attributes:nil] autorelease];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) string);
CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), NULL, CGSizeMake(rect.size.width, CGFLOAT_MAX), NULL);
Возвращаемый размер всегда имеет правильную рассчитанную ширину, однако высота всегда немного короче ожидаемой.
Это правильный способ использовать этот метод?
Есть ли другой способ верстки Core Text?
Кажется, я не единственный, кто столкнулся с проблемами с этим методом. См. https://devforums.apple.com/message/181450.
Изменить: я измерил ту же строку с кварцем, используя sizeWithFont:
, предоставляя один и тот же шрифт как для приписанной строки, так и для Quartz. Вот измерения, которые я получил:
Основной текст: 133.569336 x 16.592285
Кварц: 135,000000 х 31,000000
7 ответов
Попробуй это.. похоже на работу
+(CGFloat)heightForAttributedString:(NSAttributedString *)attrString forWidth:(CGFloat)inWidth
{
CGFloat H = 0;
// Create the framesetter with the attributed string.
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (CFMutableAttributedStringRef) attrString);
CGRect box = CGRectMake(0,0, inWidth, CGFLOAT_MAX);
CFIndex startIndex = 0;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, box);
// Create a frame for this column and draw it.
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, NULL);
// Start the next frame at the first character not visible in this frame.
//CFRange frameRange = CTFrameGetVisibleStringRange(frame);
//startIndex += frameRange.length;
CFArrayRef lineArray = CTFrameGetLines(frame);
CFIndex j = 0, lineCount = CFArrayGetCount(lineArray);
CGFloat h, ascent, descent, leading;
for (j=0; j < lineCount; j++)
{
CTLineRef currentLine = (CTLineRef)CFArrayGetValueAtIndex(lineArray, j);
CTLineGetTypographicBounds(currentLine, &ascent, &descent, &leading);
h = ascent + descent + leading;
NSLog(@"%f", h);
H+=h;
}
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);
return H;
}
Для однострочного фрейма попробуйте это:
line = CTLineCreateWithAttributedString((CFAttributedStringRef) string);
CGFloat ascent;
CGFloat descent;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
CGFloat height = ascent+descent;
CGSize textSize = CGSizeMake(width,height);
Для многострочных фреймов вам также необходимо добавить вывод строки (см. Пример кода в Руководстве по программированию основного текста)
По какой-то причине CTFramesetterSuggestFrameSizeWithConstraints()
использует разницу в подъеме и спуске для расчета высоты:
CGFloat wrongHeight = ascent-descent;
CGSize textSize = CGSizeMake(width, wrongHeight);
Это может быть ошибка?
У меня другие проблемы с шириной рамки; Стоит проверить, как это видно только в особых случаях. Смотрите этот вопрос для более.
Проблема в том, что вы должны применить стиль абзаца к тексту, прежде чем измерять его. Если вы этого не сделаете, то получите значение по умолчанию 0,0. Я предоставил пример кода для того, как это сделать, в своем ответе на дубликат этого вопроса здесь /questions/21926340/ctframesettersuggestframesizewithconstraints-inogda-vozvraschaet-nepravilnyij-razmer/21926365#21926365.
Ответ ing.conti, но в Swift 4:
var H:CGFloat = 0
// Create the framesetter with the attributed string.
let framesetter = CTFramesetterCreateWithAttributedString(attributedString as! CFMutableAttributedString)
let box:CGRect = CGRect.init(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude)
let startIndex:CFIndex = 0
let path:CGMutablePath = CGMutablePath()
path.addRect(box)
// Create a frame for this column and draw it.
let frame:CTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, nil)
// Start the next frame at the first character not visible in this frame.
//CFRange frameRange = CTFrameGetVisibleStringRange(frame);
//startIndex += frameRange.length;
let lineArray:CFArray = CTFrameGetLines(frame)
let lineCount:CFIndex = CFArrayGetCount(lineArray)
var h:CGFloat = 0
var ascent:CGFloat = 0
var descent:CGFloat = 0
var leading:CGFloat = 0
for j in 0..<lineCount {
let currentLine = unsafeBitCast(CFArrayGetValueAtIndex(lineArray, j), to: CTLine.self)
CTLineGetTypographicBounds(currentLine, &ascent, &descent, &leading)
h = ascent + descent + leading;
H+=h;
}
return H;
Я пытался сохранить его как 1:1 с кодом Objective C, но Swift не так хорош при работе с указателями, поэтому некоторые изменения были необходимы для приведения.
Я также сделал несколько тестов, сравнивая этот код (и его аналог ObjC) с другими методами высоты. В качестве главы я использовал ОГРОМНУЮ и очень сложную атрибутивную строку в качестве входных данных, а также сделал это на симе, поэтому сами времена не имеют смысла, однако относительные скорости верны.
Runtime for 1000 iterations (ms) BoundsForRect: 8909.763097763062
Runtime for 1000 iterations (ms) layoutManager: 7727.7010679244995
Runtime for 1000 iterations (ms) CTFramesetterSuggestFrameSizeWithConstraints: 1968.9229726791382
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame ObjC: 1941.6030206680298
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame-Swift: 1912.694974899292
Воскрешая.
При первоначальном определении того, где в кадре должны быть размещены линии, базовый текст, по-видимому, массирует всплытие + спуск для целей расчета начала линии. В частности, кажется, что к восхождению добавляется 0,2*(подъем + спуск), а затем как спуск, так и результирующий подъем изменяются floor(x + 0.5)
, а затем исходные позиции рассчитываются на основе этих скорректированных подъемов и спусков. На оба этих шага влияют определенные условия, характер которых я не уверен, и я также уже забыл, в какой момент стили абзацев принимаются во внимание, несмотря на то, что я рассматривал их только несколько дней назад.
Я уже смирился с тем, что просто рассматриваю линию, начинающуюся с ее базовой линии, и не пытаюсь выяснить, на что на самом деле приземляются линии. К сожалению, этого все еще не достаточно: стили абзаца не отражаются в CTLineGetTypographicBounds()
и некоторые шрифты, такие как Klee, имеющие ненулевые начертания, пересекают прямоугольник пути! Не уверен, что с этим делать... вероятно, для другого вопроса.
ОБНОВИТЬ
Похоже на то CTLineGetBoundsWithOptions(line, 0)
действительно получает правильные границы линий, но не совсем полностью: между строками есть разрыв, а с некоторыми шрифтами (опять же, кли) разрыв отрицательный и линии перекрываются... Не уверен, что с этим делать.:| По крайней мере, мы немного ближе?
И даже тогда это все еще не принимает во внимание стили абзаца>:|
CTLineGetBoundsWithOptions()
не указан на сайте документации Apple, возможно, из-за ошибки в текущей версии их генератора документации. Однако это полностью документированный API - вы найдете его в заголовочных файлах, и он подробно обсуждался на сессии 226 WWDC 2012 года.
Ни один из вариантов не имеет отношения к нам: они уменьшают прямоугольник границ, принимая во внимание определенные варианты дизайна шрифта (или увеличивают прямоугольник границ случайным образом, в случае нового kCTLineBoundsIncludeLanguageExtents
). В общем, одна полезная опция kCTLineBoundsUseGlyphPathBounds
, что эквивалентно CTLineGetImageBounds()
но без необходимости указывать CGContext
(и, следовательно, не подчиняясь существующей текстовой матрице или CTM).
После нескольких недель попыток всего, любых возможных комбинаций, я сделал прорыв и нашел то, что работает. Эта проблема кажется более заметной в macOS, чем в iOS, но по-прежнему возникает в обоих случаях.
Что сработало для меня, так это использоватьвместоNSTextField
(в macOS) илиUILabel
(на iOS).
И используя boundingRect(with:options:context:) вместоCTFramesetterSuggestFrameSizeWithConstraints
. Несмотря на то, что теоретически последний должен быть более низким уровнем, чем первый, и я предполагал, что он будет более точным, переломным моментом оказывается NSString.DrawingOptions.usesDeviceMetrics.
Предлагаемый размер рамы подходит как шарм.
Пример:
let attributedString = NSAttributedString(string: "my string")
let maxWidth = CGFloat(300)
let size = attributedString.boundingRect(
with: .init(width: maxWidth,
height: .greatestFiniteMagnitude),
options: [
.usesFontLeading,
.usesLineFragmentOrigin,
.usesDeviceMetrics])
let textLayer = CATextLayer()
textLayer.frame = .init(origin: .zero, size: size)
textLayer.contentsScale = 2 // for retina
textLayer.isWrapped = true // for multiple lines
textLayer.string = attributedString
Затем вы можете добавитьCATextLayer
любомуNSView
/UIView
.
macOS
let view = NSView()
view.wantsLayer = true
view.layer?.addSublayer(textLayer)
iOS
let view = UIView()
view.layer.addSublayer(textLayer)
Это может показаться странным, но я обнаружил, что если вы используете ceil
сначала используйте функцию, а затем добавьте +1 к высоте, это всегда будет работать. Многие сторонние API используют этот трюк.