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 используют этот трюк.

Другие вопросы по тегам