Выравнивание глифов в верхней части UITextView после sizeToFit - PullRequest
4 голосов
/ 06 марта 2019

Приложение, над которым я работаю, поддерживает сотни различных шрифтов. Некоторые из этих шрифтов, особенно шрифты сценариев, имеют значительные восходящие и нисходящие. Когда sizeToFit() вызывается для UITextView с некоторыми из этих шрифтов, я получаю значительный верхний и нижний отступы (изображение слева). Цель состоит в том, чтобы получить изображение справа, чтобы самый высокий глиф был выровнен заподлицо с верхней частью ограничивающего прямоугольника текстового представления.

UITextView Layout Examples

Вот журнал для изображения выше:

Point Size: 59.0
Ascender:  70.21
Descender:  -33.158
Line Height:  103.368
Leading: 1.416
TextView Height: 105.0

Моей первой мыслью было посмотреть на высоту каждого глифа в первой строке текста, а затем вычислить смещение между вершиной контейнера и вершиной самого высокого глифа. Тогда я мог бы использовать textContainerInset для соответствующей корректировки верхнего поля.

Я пытался что-то подобное в моем UITextView подклассе:

for location in 0 ..< lastGlyphIndexInFirstLine {
    let glphyRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: location, length: 1), in: self.textContainer)
    print(glphyRect.size.height) // prints 104.78399999999999 for each glyph
}

К сожалению, это не работает, потому что boundRect(forGlyphRange:in:) не возвращает прямоугольник самого глифа (я предполагаю, что это всегда одно и то же значение, потому что оно возвращает высоту фрагмента строки?).

Это самый простой способ решить эту проблему? Если это так, как я могу рассчитать расстояние между вершиной текстового представления и вершиной самого высокого глифа в первой строке текста?

1 Ответ

2 голосов
/ 09 марта 2019

Это не представляется возможным, используя TextKit, но это возможно, используя CoreText напрямую. В частности, CGFont getGlyphBBoxes возвращает правильный прямоугольник в глиф пробелах , который затем можно преобразовать в точки относительно размера шрифта.

Кредит идет к этому ответу за то, что я узнал о getGlyphBBoxes, а также за документирование того, как преобразовать полученные ректы в баллы.

Ниже приведено полное решение. Это предполагает, что у вас есть подкласс UITextView со следующим заданным заранее:

self.contentInset = .zero
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0.0

Эта функция теперь возвращает расстояние от вершины границ текстового представления до вершины самого высокого используемого глифа:

private var distanceToGlyphs: CGFloat {
    // sanity
    guard
        let font = self.font,
        let fontRef = CGFont(font.fontName as CFString),
        let attributedText = self.attributedText,
        let firstLine = attributedText.string.components(separatedBy: .newlines).first
    else { return 0.0 }

    // obtain the first line of text as an attributed string
    let attributedFirstLine = attributedText.attributedSubstring(from: NSRange(location: 0, length: firstLine.count)) as CFAttributedString

    // create the line for the first line of attributed text
    let line = CTLineCreateWithAttributedString(attributedFirstLine)

    // get the runs within this line (there will typically only be one run when using a single font)
    let glyphRuns = CTLineGetGlyphRuns(line) as NSArray
    guard let runs = glyphRuns as? [CTRun] else { return 0.0 }

    // this will store the maximum distance from the baseline
    var maxDistanceFromBaseline: CGFloat = 0.0

    // iterate each run
    for run in runs {
        // get the total number of glyphs in this run
        let glyphCount = CTRunGetGlyphCount(run)

        // initialize empty arrays of rects and glyphs
        var rects = Array<CGRect>(repeating: .zero, count: glyphCount)
        var glyphs = Array<CGGlyph>(repeating: 0, count: glyphCount)

        // obtain the glyphs
        self.layoutManager.getGlyphs(in: NSRange(location: 0, length: glyphCount), glyphs: &glyphs, properties: nil, characterIndexes: nil, bidiLevels: nil)

        // obtain the rects per-glyph in "glyph space units", each of which needs to be scaled using units per em and the font size
        fontRef.getGlyphBBoxes(glyphs: &glyphs, count: glyphCount, bboxes: &rects)

        // iterate each glyph rect
        for rect in rects {
            // obtain the units per em from the font ref so we can convert the rect
            let unitsPerEm = CGFloat(fontRef.unitsPerEm)

            // sanity to prevent divide by zero
            guard unitsPerEm != 0.0 else { continue }

            // calculate the actual distance up or down from the glyph's baseline
            let glyphY = (rect.origin.y / unitsPerEm) * font.pointSize

            // calculate the actual height of the glyph
            let glyphHeight = (rect.size.height / unitsPerEm) * font.pointSize

            // calculate the distance from the baseline to the top of the glyph
            let glyphDistanceFromBaseline = glyphHeight + glyphY

            // store the max distance amongst the glyphs
            maxDistanceFromBaseline = max(maxDistanceFromBaseline, glyphDistanceFromBaseline)
        }
    }

    // the final top margin, calculated by taking the largest ascender of all the glyphs in the font and subtracting the max calculated distance from the baseline
    return font.ascender - maxDistanceFromBaseline
}

Теперь вы можете установить верхнюю часть текстового представления contentInset на -distanceToGlyphs для достижения желаемого результата.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...