- PVSM.RU - https://www.pvsm.ru -
Я хотел бы Вам рассказать про вывод текста на экран в iOS. Вначале это задумывалось как туториал, затем как сбор неочевидных фактов, а получилось что получилось.
Способы вывода текста условно можно разделить на несколько категорий:
Перво-наперво, нужно рассказать о базовой вещи, которая называется NSAttributedString или CFAttributedStringRef если мы работаем с toll-free-bridge объектами. Это строка, которой можно задавать атрибуты отображения.
NSDictionary *textAttributes = @{(NSString *)kCTFontAttributeName : [UIFont systemFontOfSize:16]};
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:LoremIpsum attributes:textAttributes];
Этот код создает строку с единственным аттрибутом отображения: шрифтом, который необходимо использовать при отрисовке строки. Оно выглядит примерно так:
Доступных стандартных атрибутов довольно много:
Например, текст с кернингом 3.0, размером шрифта 14 и подчеркиванием, отступом всего текста справа на 20.0 пунктов, красной строкой на 40.0 пунктов и расстоянием между параграфами в 30.0 пунктов:
CTParagraphStyleSetting paragraphSettings[] = (CTParagraphStyleSetting[]){
(CTParagraphStyleSetting){ kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(float_t), (float_t[]){ 40.0f } },
(CTParagraphStyleSetting){ kCTParagraphStyleSpecifierHeadIndent, sizeof(float_t), (float_t[]){ 20.0f } },
(CTParagraphStyleSetting){ kCTParagraphStyleSpecifierParagraphSpacing, sizeof(float_t), (float_t[]){ 30.0f } }
};
id paragraphStyle = (__bridge_transfer id)CTParagraphStyleCreate(paragraphSettings, sizeof(paragraphSettings) / sizeof(paragraphSettings[0]));
NSDictionary *textAttributes = @{
(NSString *)kCTFontAttributeName : [UIFont systemFontOfSize:14],
(NSString *)kCTKernAttributeName : @(3.0),
(NSString *)kCTUnderlineStyleAttributeName : @(kCTUnderlineStyleSingle),
(NSString *)kCTParagraphStyleAttributeName : paragraphStyle
};
Теперь о том, как же это все можно отрисовывать. Начиная с iOS6 в UIKit у NSAttributedString появилась категория NSStringDrawing и можно просто сделать так:
NSDictionary *textAttributes = @{(NSString *)kCTFontAttributeName : [UIFont systemFontOfSize:16]};
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:LoremIpsum attributes:textAttributes];
[attributedString drawInRect:rect];
Есть еще один метод:
- (void)drawWithRect:(CGRect)rect options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context;
который прикидывается якобы настраиваемым при помощи передаваемых параметров и специального контекста. Откровенно говоря, я никогда не использовал эти методы, потому что минимальная версия iOS, которую мы поддерживаем в нашем продукте — это 4.3.
Так же можно передать созданный NSAttributedString в UILabel, UITextField или UITextView через метод setAttributedString:. Этот метод тоже поддерживается только с iOS6.0. Кстати, для этих элементов, начиная с iOS6.0, атрибуты для отображения можно задавать прямо в InterfaceBuilder в xib или storyboard.
CoreText — это такая крутая штука, которая позволяет отрисовывать текст на нескольких уровнях абстракций. Также она позволяет на нескольких уровнях получать доступ к элементарным частям отображения текста.
Прежде чем я начну рассказывать дальше, хочу сказать что к этому моменту подразумевается, что Вы знаете что такое графический контекст и знакомы с афинными преобразованями.
Еще я украду картинку с сайта эппл:
На этом уровне мы еще не можем рисовать. Единственная роль этой абстракции: создавать объекты для рисования, которые будут вписаны в геометрические примитивы, используя текст с заданными стилями. Также с помощью CTFramesetter можно, зафиксировав ширину или высоту, узнать высоту или ширину для текущего текста с текущими параметрами отображения.
NSDictionary *textAttributes = @{(NSString *)kCTFontAttributeName : [UIFont systemFontOfSize:16]};
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:LoremIpsum attributes:textAttributes];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)(attributedString));
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(CGRectGetWidth(rect), CGFLOAT_MAX), NULL);
Здесь зафиксирована ширина, в которую нужно вписать текст. suggestedSize будет содержать искомую высоту текста. То же можно сделать и с шириной.
Этот объект создает описанным выше CTFramesetter и он уже полностью готов к отрисовке.
CGPathRef path = CGPathCreateWithRect((CGRect){CGPointZero, suggestedSize}, NULL);
CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CTFrameDraw(textFrame, context);
Хочу подробнее остановиться на параметрах следующего метода:
CTFrameRef CTFramesetterCreateFrame(
CTFramesetterRef framesetter,
CFRange stringRange,
CGPathRef path,
CFDictionaryRef frameAttributes )
Парочка примеров.
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, NULL, (CGRect){CGPointZero, CGSizeMake(suggestedSize.width, suggestedSize.height / 2)});
CGPathAddEllipseInRect(path, NULL, (CGRect){0, suggestedSize.height / 2, CGSizeMake(suggestedSize.width, suggestedSize.height / 2)});
CGAffineTransform transformation = CGAffineTransformMakeRotation(M_PI_4);
CGPathAddEllipseInRect(path, &transformation, CGRectMake(20, -200, 400, 130));
CGPathAddRect(path, &transformation, CGRectMake(50, -150, 200, 200));
В последнем примере также используется параметр kCTFramePathFillWindingNumber для CTFrame, поэтому пересекаемые области тоже заполнены текстом.
Еще два важных момента:
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGContextTranslateCTM(context, 0.0f, -suggestedSize.height);
Вот что будет с не единичной матрицой трансформации:
CGContextSetTextMatrix(context, CGAffineTransformMakeRotation(M_PI_4));
CGContextScaleCTM(context, 1.0f, -1.0f);
CGContextTranslateCTM(context, 0.0f, -suggestedSize.height);
Перед дальнейшим расказом я бы хотел украсть еще одну картинку с сайта эппл:
Именно эта сущность участвует в создании CTFrame. Ее роль — создание тех самых текстовых графических примитивов, которые уже готовы к отрисовке на данном этапе. Он осуществляет перенос строк по заданному алгоритму (по словам, символам, etc), обрезает все что не влезло.
Это то, из чего состоит CTFrame. CTLine можно отрисовывать, задавая позиции по котрым они и будут расположены: беря расчитанные из CTFrame при помощи метода CTFrameGetLineOrigins или же задавая по какому-то Очень Хитрому Алгоритму.
CFArrayRef lines = CTFrameGetLines(textFrame);
for (CFIndex i = 0, linesCount = CFArrayGetCount(lines); i < linesCount; ++i) {
CGPoint lineOrigin = CGPointZero;
CTFrameGetLineOrigins(textFrame, CFRangeMake(i, 1), &lineOrigin);
CGContextSetTextPosition(context, lineOrigin.x, lineOrigin.y);
CTLineDraw(CFArrayGetValueAtIndex(lines, i), context);
}
Что еще обязательно стоит знать. У линии есть несколько типографских параметров: baseline, ascent, descent, leading. Про каждый можно почитать здесь [4]. CTFrameGetLineOrigins возвращает именно baseline. Остальные параметры можно узнать через метод:
doubCTLineGetTypographicBounds(CTLineRef line, CGFloat* ascent, CGFloat* descent, CGFloat* leading);
Красный цвет — descent, черный — baseline, синий — ascent.
К этим параметрам мы еще вернемся. А пока хочу заметить, что descent и ascent будут браться для всей строки как минимальный и максимальный для всех шрифтов, которые использованы в строке. Например, для приведенного выше текста, хорошо заметно, что в строке, где присутствует иврит, descent больше.
Еще несколько полезных методов:
CFRange CTLineGetStringRange(CTLineRef line)
Возвращает для текущей линии ее строковый интервал. Очень часто используется в алгоритмах, когда нужны только лишь определенные линии попадающие в выделенный строковый интервал.
CFIndex CTLineGetStringIndexForPosition(CTLineRef line, CGPoint position)
Возвращает строковый индекс для указанной позиции в строке. Например, может использоваться для определения нажатия на определенные области текста. Обратите внимание, что контекст перевернутый, и (0,0) будет находиться в самой нижней строке.
CTLineRef CTLineCreateWithAttributedString(CFAttributedStringRef string)
Для отрисовки линии не обязательно создавать всю цепочку из CTFramesetter->CTFrame->CTLine. Можно создать сразу и линию. Довольно часто возникает задача также ее обрезать, если текст не вмещается в заданную область:
CTLineRef CTLineCreateTruncatedLine(CTLineRef line, double width, CTLineTruncationType truncationType, CTLineRef truncationToken)
Наверное, самая полезная абстракция для того, кто пишет свой элемент для ввода текста. CTRun — это то, во что группируются символы с одинаковыми стилями для отрисовки группами. Вернее группируются начиная с iOS6.0, а во всем что младше — каждый символ будет представлять собой отдельный CTRun. С самого начала, нужно уяснить один легкий и очевидный момент: есть символы, а есть глифы [5]. Из этого факта следуют еще два момента, тоже легких, но уже неочевидных: один символ может состоять из одного глифа, а может и из нескольких; один глиф может представлять сразу несколько символов. Например, большинство смайликов представляют собой один глиф, но состоят из нескольких UTF символов.
Всем тем, кто собирается работать с CTRun я строго рекомендую ознакомиться с CTRun.h – все методы которые там есть исключительно полезны.
CFIndex CTRunGetGlyphCount(CTRunRef run)
Возвращает количество глифов в CTRun.
CFRange CTRunGetStringRange(CTRunRef run)
Возвращает диапазон от всей строки в котором расположен CTRun. Длина этого диапазона не всегда равна результату предыдущего метода!
const CFIndex* CTRunGetStringIndicesPtr(CTRunRef run)
void CTRunGetStringIndices(CTRunRef run, CFRange range, CFIndex buffer[])
Этот метод связан с предыдущим. Он возвращает позиции глифов в строке. Например, есть текст из нескольких смайликов:
Начиная с iOS6.0 CTrun будет следующим:
CTRun: string range = (0, 6), string = "U0001f437U0001f434U0001f428"
Как видно, каждый глиф состоит на самом деле из двух символов. Результат вызова метода CTRunGetStringIndices будет: [0,2,4].
Обратите внимание, что этот метод есть в двух вариантах: скопировать в выделенный заранее буфер нужное количество индексов или получить указатель на начало массива внутри CTRun. Выбирайте, что Вам удобнее. Таких методов довольно много, это видно из сигнатур, и я не буду заострять на этом внимание.
CFDictionaryRef CTRunGetAttributes(CTRunRef run)
Текущие атрибуты для CTRun, которые были заданы в CFAttributedString.
CTRunStatus CTRunGetStatus(CTRunRef run)
В виде битовой маски возвращает специфичные для отображения параметры. Обратите особое внимание на kCTRunStatusRightToLeft – показывает, что глифы в CTRun имеют направление написания справа налево.
const CGPoint* CTRunGetPositionsPtr(CTRunRef run)
void CTRunGetPositions(CTRunRef run, CFRange range, CGPoint buffer[])
Позиции для глифов внутри CTRun относительно CTFrame. Особое внимание обращаю на то, что если Вы будете пользоваться вторым вариантом метода и захотите взять позиции, скажем, второго глифа, то в range нужно передать не 1 (нумерация начинается с 0), а позицию этого глифа в строке. Для варианта со смайликами, который был выше, это была бы позииция 2. Это касается всех аналогичных методов.
const CGSize* CTRunGetAdvancesPtr(CTRunRef run)
void CTRunGetAdvances(CTRunRef run, CFRange range, CGSize buffer[])
Возращает размеры глифов.
double CTRunGetTypographicBounds(CTRunRef run, CFRange range, CGFloat* ascent, CGFloat* descent, CGFloat* leading)
Возвращает типографские параметры для отдельного CTRun. Помните я говорил, что для строки descent и ascent будут браться как крайние для шрифтов, которые были использованы в этой строке? Так вот, теперь для каждой части строки их можно узнать отдельно:
Еще, когда стало понятно что такое descent и ascent я расскажу о атрибуте kCTRunDelegateAttributeName. На вход он принимает CTRunDelegate, который создается на основе ряда методов:
typedef struct {
CFIndex version;
CTRunDelegateDeallocateCallback dealloc;
CTRunDelegateGetAscentCallback getAscent;
CTRunDelegateGetDescentCallback getDescent;
CTRunDelegateGetWidthCallback getWidth;
} CTRunDelegateCallbacks;
Нас интересуют последние три. Методы представляют собой свободные функции, которые возвращают descent, ascent и ширину глифа, который был промаркирован этим атрибутом. Набор методов будет вызываться каждый раз, когда параметры текущего глифа должны участвовать в расчете общих геометрических параметров. Это может быть применено для создания своих глифов.
void CTRunDraw(CTRunRef run, CGContextRef context, CFRange range)
Нарисовать CTRun в графический контекст.
Откровенно говоря, этот уровень уже не CoreText, а CoreGraphics — он был доступен и раньше. Просто сейчас удобнее стало доставать глифы.
CFIndex glyphCount = CTRunGetGlyphCount(run);
CGPoint positions[glyphCount];
CGGlyph glyphs[glyphCount];
CTRunGetPositions(run, CFRangeMake(0, 0), positions);
CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs);
CGContextSetFont(context, cgFont);
CGContextSetFontSize(context, CTFontGetSize(runFont));
CGContextSetFillColorWithColor(context, runColor);
CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);
Надо понимать, что в этом случае не будут доступны вещи вроде стилей подчеркивания, поскольку они реализуются посредством CoreText. Зато при отрисовке мы вольны как угодно располагать глифы.
Apple, как всегда, верна своим традициям в проектировании API: для простых вещей можно далеко не лезть и просто вызвать метод “сделайМнеХорошо”, а для более сложных есть возможность покрутить веревочки и подергать колесики.
Автор: prizzrak
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios-development/29392
Ссылки в тексте:
[1] лигатур: http://ru.wikipedia.org/wiki/%D0%9B%D0%B8%D0%B3%D0%B0%D1%82%D1%83%D1%80%D0%B0_(%D1%81%D0%BE%D0%B5%D0%B4%D0%B8%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B1%D1%83%D0%BA%D0%B2)
[2] этому документу: http://www.unicode.org/reports/tr9/
[3] пересекаемых областях: http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_paths/dq_paths.html#//apple_ref/doc/uid/TP30001066-CH211-TPXREF106
[4] здесь: http://ru.wikipedia.org/wiki/%D0%91%D0%B0%D0%B7%D0%BE%D0%B2%D0%B0%D1%8F_%D0%BB%D0%B8%D0%BD%D0%B8%D1%8F_(%D1%82%D0%B8%D0%BF%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D0%BA%D0%B0)
[5] глифы: http://ru.wikipedia.org/wiki/%D0%93%D0%BB%D0%B8%D1%84
[6] Источник: http://habrahabr.ru/post/172661/
Нажмите здесь для печати.