Нежелательное вертикальное заполнение из iOS 6 на CATextLayer

Предыстория: я начал свой проект в iOS 5 и создал красивую кнопку со слоем. Я добавил textLayer на кнопку и центрировал ее, используя следующий код:

    float textLayerVerticlePadding = ((self.bounds.size.height - fontSize) /2);
    textLayer = [[CATextLayer alloc]init];
    [textLayer setFrame:CGRectOffset(self.bounds, 0, textLayerVerticlePadding)];

Он отлично работает и выглядит точно по центру до iOS 6.

Проблема: iOS 6 добавила пробел (отступ) между самой верхней границей и текстом в textLayer. Это нарушает приведенный выше расчет. Есть ли способ убедиться, что iOS 6 не работает? потому что я хотел бы поддерживать как iOS 5, так и 6 (для тех, кто предпочитает Google Map).

Картинки:
Это iOS 5, а красный цвет — это фон текстового слоя (чтобы сделать его более заметным) введите здесь описание изображения

А это iOS 6
введите здесь описание изображения


Обновление: Хотя я уверен, что все приведенные ниже ответы по-своему верны, я нашел пост от t0rst самым простым способом выполнить это. HelveticaNeue оставляет немного места как для iOS5, так и для iOS6, в отличие от Helvetica, который не оставляет места вверху в iOS5 и мало места в iOS6.

Обновление 2: еще немного поиграл с ним и выяснил размер небольшого пространства. Не вдаваясь в подробности, пространство составляет 1/6 размера вашего шрифта. Поэтому, чтобы компенсировать это, я написал

float textLayerVerticlePadding = ((self.bounds.size.height - fontSize) /2) - (fontSize/6);
[textLayer setFrame:CGRectOffset(self.bounds, 0, textLayerVerticlePadding)];

С этим кодом я каждый раз попадаю в мертвую точку. Обратите внимание, что это проверено только с HelveticaNeue-Bold на iOS5 и iOS6. Я не могу сказать ни о чем другом.


person Byte    schedule 24.09.2012    source источник
comment
Быстрый ответ: используйте семейство HelveticaNeue вместо системных шрифтов (имя шрифта .HelveticaNeueUI). Длинный ответ: см. ниже.   -  person t0rst    schedule 19.10.2012
comment
Привет, а что, если я хочу добавить больше отступов вверху, чтобы сделать HELLO в центре красного поля, т.е. вертикальное выравнивание = центр?   -  person Van Du Tran    schedule 30.04.2014


Ответы (5)


В iOS 5 и более ранних версиях первая базовая линия в CATextLayer всегда позиционируется вниз от верхней границы границ путем подъема, полученного из CTLineGetTypographicBounds при передаче CTLine, сделанной со строкой для первой строки.

В iOS 6 это больше не относится ко всем шрифтам. Следовательно, когда вы позиционируете CATextLayer, вы больше не можете надежно решить, куда его поместить, чтобы получить правильное визуальное выравнивание. Или ты можешь? ...

Во-первых, небольшое отступление: некоторое время назад, когда я пытался отработать поведение позиционирования CATextLayer в iOS 5, я пытался использовать все комбинации высоты крышки, зажима из UIFont и т. д., прежде чем, наконец, обнаружил, что восхождение от CTLineGetTypographicBounds было тем, что мне было нужно. В процессе я обнаружил, что а) подъем от UIFont ascender, CTFontGetAscent и CTLineGetTypographicBounds непостоянен для определенных шрифтов, и б) подъем часто бывает странным — либо обрезает акценты, либо оставляет слишком много места вверху. Решение а) состоит в том, чтобы знать, какое значение использовать. На самом деле нет решения b), кроме как оставить много места выше, сместив границы CATextLayer, если, вероятно, у вас будут акценты, которые будут обрезаны.

Вернёмся к iOS 6. Если вы избегаете наиболее оскорбительных шрифтов (начиная с версии 6.0 и, вероятно, они могут быть изменены), вы всё равно можете программно позиционировать CATextLayer с остальными шрифтами. Нарушителями являются: AcademyEngravedLetPlain, Courier, HoeflerText и Palatino — визуально эти семейства расположены правильно (т.е. без отсечения ) в CATextLayer, но ни один из трех источников подъема не дает удобного указания на то, где находится базовая линия. Семейства Helvetica и .HelveticaNeueUI (также известные как системные шрифты) правильно располагаются с базовой линией на подъеме, заданном UIFont ascender, но другие источники подъема не используются.

Некоторые примеры из тестов, которые я сделал. Образец текста рисуется трижды разными цветами. Начало координат находится в левом верхнем углу серого прямоугольника. Черный текст отрисовывается на CTLineDraw со смещением вниз по восхождению от CTLineGetTypographicBounds; прозрачный красный рисуется CATextLayer с границами, равными серому прямоугольнику; прозрачный синий нарисован с добавлением UIKit NSString drawAtPoint:withFont:, расположенным в начале серого поля, и с UIFont.

1) Хорошо себя зарекомендовавший шрифт Copperplate-Light. Три образца совпадают, что дает темно-бордовый цвет и означает, что всплытия практически одинаковы из всех источников. То же самое для iOS 5 и 6.

copperplate-light iOS 6

2) Курьер под iOS 5. CATextLayer позиционирует текст слишком высоко (красный), но CTLineDraw с подъемом от CTLineGetTypographicBounds (черный) соответствует позиционированию CATextLayer, поэтому мы можем разместить и исправить оттуда. NSString drawAtPoint:withFont: (синий) размещает текст без обрезки. (Helvetica и .HelveticaNeueUI ведут себя так же в iOS 6)

курьер iOS 5

3) Курьер под iOS 6. CATextLayer (красный) теперь размещает текст так, чтобы он не обрезался, но позиционирование больше не соответствует восхождению от CTLineGetTypographicBounds (черный) или от UIFont восходящего элемента, используемого в NSString drawAtPoint:withFont: ( синий). Это непригодно для программного позиционирования. (AcademyEngravedLetPlain, HoeflerText и Palatino также ведут себя так же в iOS 6)

курьер iOS 6

Надеюсь, это поможет избежать некоторых часов потраченного впустую времени, которые я потратил, и если вы хотите погрузиться немного глубже, поиграйте с этим:

- (NSString*)reportInconsistentFontAscents
{
    NSMutableString*            results;
    NSMutableArray*             fontNameArray;
    CGFloat                     fontSize = 28;
    NSString*                   fn;
    NSString*                   sample = @"Éa3Çy";
    CFRange                     range;
    NSMutableAttributedString*  mas;
    UIFont*                     uifont;
    CTFontRef                   ctfont;
    CTLineRef                   ctline;
    CGFloat                     uif_ascent;
    CGFloat                     ctfont_ascent;
    CGFloat                     ctline_ascent;

    results = [NSMutableString stringWithCapacity: 10000];
    mas = [[NSMutableAttributedString alloc] initWithString: sample];
    range.location = 0, range.length = [sample length];

    fontNameArray = [NSMutableArray arrayWithCapacity: 250];
    for (fn in [UIFont familyNames])
        [fontNameArray addObjectsFromArray: [UIFont fontNamesForFamilyName: fn]];
    [fontNameArray sortUsingSelector: @selector(localizedCaseInsensitiveCompare:)];
    [fontNameArray addObject: [UIFont systemFontOfSize: fontSize].fontName];
    [fontNameArray addObject: [UIFont italicSystemFontOfSize: fontSize].fontName];
    [fontNameArray addObject: [UIFont boldSystemFontOfSize: fontSize].fontName];

    [results appendString: @"Font name\tUIFA\tCTFA\tCTLA"];

    for (fn in fontNameArray)
    {
        uifont = [UIFont fontWithName: fn size: fontSize];
        uif_ascent = uifont.ascender;

        ctfont = CTFontCreateWithName((CFStringRef)fn, fontSize, NULL);
        ctfont_ascent = CTFontGetAscent(ctfont);

        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)mas, range, kCTFontAttributeName, ctfont);
        ctline = CTLineCreateWithAttributedString((CFAttributedStringRef)mas);
        ctline_ascent = 0;
        CTLineGetTypographicBounds(ctline, &ctline_ascent, 0, 0);

        [results appendFormat: @"\n%@\t%.3f\t%.3f\t%.3f", fn, uif_ascent, ctfont_ascent, ctline_ascent];

        if (fabsf(uif_ascent - ctfont_ascent) >= .5f // >.5 can round to pixel diffs in display
         || fabsf(uif_ascent - ctline_ascent) >= .5f)
            [results appendString: @"\t*****"];

        CFRelease(ctline);
        CFRelease(ctfont);
    }

    [mas release];

    return results;
}
person t0rst    schedule 18.10.2012
comment
Здравствуйте, возможно ли вертикальное выравнивание по центру Привет в красном поле? - person Van Du Tran; 30.04.2014
comment
@VanDuTran Ответ выше касается предсказания (насколько это возможно надежно) вертикального положения базовой линии вашего текста, когда ваш CATextLayer.bounds.origin равен 0,0. Как только вы это узнаете, вы сможете решить, где вы на самом деле хотите расположить базовую линию (используя высоту шрифта, подъем, доступную область и т. д.) для эффекта, которого вы хотите достичь, а затем вы настраиваете CATextLayer.bounds. origin.y, чтобы сместить базовую линию в нужное положение. - person t0rst; 02.05.2014
comment
Я попробую, потому что в моем случае я хочу, чтобы красный прямоугольник был виден как фон для текстового слоя. Итак, я хочу белый текст в середине красного поля. - person Van Du Tran; 02.05.2014
comment
Как это помогает центрировать текст внутри CATextLayer? я не понимаю - person Spail; 13.02.2015
comment
@spail Если ваш дисплей статичен, просто отрегулируйте размер и номера позиций вручную, пока они не будут в порядке. Если вам нужно позиционировать динамически, вам нужно знать, где CATextLayer помещает базовую линию, когда вы используете заданный UIFont в заданных границах, чтобы вы могли программно настроить размеры и смещения для желаемого эффекта. CATextLayer не сообщает вам, где находится базовая линия. Этот ответ о том, как с максимальной надежностью предсказать, где CATextLayer поместит текст. Как только вы это узнаете, ваше программное позиционирование может последовать за вами. - person t0rst; 13.02.2015
comment
@ t0rst ах, да, я использовал неправильные значения. похоже, что для вертикального центрирования текста внутри слоя вы должны настроить его границы origin.y на (textLayer.bounds.size.height - myFont.capHeight) / 2.0 - (myFont.ascender - myFont.capHeight) / 2.0 - person Spail; 13.02.2015

Ответ t0rst помогает мне. Я думаю, что capHeight и xHeight являются ключевыми.

    CATextLayer *mytextLayer = [CATextLayer layer];
    CGFloat fontSize = 30;
    UIFont *boldFont = [UIFont boldSystemFontOfSize:fontSize];
    mytextLayer.font = (__bridge CFTypeRef)(boldFont.fontName);
    mytextLayer.fontSize = fontSize;

    CGFloat offsetY = 0;

    //if system version is grater than 6
    if(([[[UIDevice currentDevice] systemVersion] compare:@"6" options:NSNumericSearch] == NSOrderedDescending)){
        offsetY = -(boldFont.capHeight - boldFont.xHeight);
    }

    //you have to set textX, textY, textWidth
    mytextLayer.frame = CGRectMake(textX, textY + offsetY, textWidth, fontSize);
person kinkuma    schedule 21.11.2012

Пока я жду окончательного решения, я изучил RTLabel и TTTAttributedLabel и создал простой класс для рисования текста на CALayer, как предложил Стив. Надеюсь, это поможет, и, пожалуйста, не стесняйтесь указать на любую ошибку, которую я сделал.

CustomTextLayer.h

#import <QuartzCore/QuartzCore.h>

@interface CustomTextLayer : CALayer {
    NSString                        *_text;
    UIColor                         *_textColor;

    NSString                        *_font;
    float                           _fontSize;

    UIColor                         *_strokeColor;
    float                           _strokeWidth;

    CTTextAlignment                 _textAlignment;
    int                             _lineBreakMode;

    float                           _suggestHeight;
}

-(float) suggestedHeightForWidth:(float) width;

@property (nonatomic, retain) NSString *text;
@property (nonatomic, retain) UIColor *textColor;

@property (nonatomic, retain) NSString *font;
@property (nonatomic, assign) float fontSize;

@property (nonatomic, retain) UIColor *strokeColor;
@property (nonatomic, assign) float strokeWidth;

@property (nonatomic, assign) CTTextAlignment textAlignment;

@end

CustomTextLayer.m

#import <CoreText/CoreText.h>
#import "CustomTextLayer.h"

@implementation CustomTextLayer

@synthesize text = _text, textColor = _textColor;
@synthesize font = _font, fontSize = _fontSize;
@synthesize strokeColor = _strokeColor, strokeWidth = _strokeWidth;
@synthesize textAlignment = _textAlignment;

-(id) init {
    if (self = [super init]) {
        _text = @"";
        _textColor = [UIColor blackColor];

        _font = @"Helvetica";
        _fontSize = 12;

        _strokeColor = [UIColor whiteColor];
        _strokeWidth = 0.0;

        _textAlignment = kCTLeftTextAlignment;
        _lineBreakMode = kCTLineBreakByWordWrapping;

    }
    return self;
}

-(void) dealloc {
    [_text release];
    [_textColor release];

    [_font release];

    [_strokeColor release];

    [super dealloc];
}

-(void) setText:(NSString *)text {
    [_text release];
    _text = [text retain];
    [self setNeedsDisplay];
}

-(void) setTextColor:(UIColor *)textColor {
    [_textColor release];
    _textColor = [textColor retain];
    [self setNeedsDisplay];
}

-(void) setFont:(NSString *)font {
    [_font release];
    _font = [font retain];
    [self setNeedsDisplay];
}

-(void) setFontSize:(float)fontSize {
    _fontSize = fontSize;
    [self setNeedsDisplay];
}

-(void) setStrokeColor:(UIColor *)strokeColor {
    [_strokeColor release];
    _strokeColor = strokeColor;
    [self setNeedsDisplay];
}

-(void) setStrokeWidth:(float)strokeWidth {
    _strokeWidth = 0 ? (strokeWidth < 0) : (-1 * strokeWidth);
    [self setNeedsDisplay];
}

-(void) setTextAlignment:(CTTextAlignment)textAlignment {
    _textAlignment = textAlignment;
    [self setNeedsDisplay];
}

-(void) setFrame:(CGRect)frame {
    [super setFrame: frame];
    [self setNeedsDisplay];
}

-(float) suggestedHeightForWidth:(float) width {

    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)_font, _fontSize, NULL);

    CTParagraphStyleSetting paragraphStyles[2] = {
        {.spec = kCTParagraphStyleSpecifierLineBreakMode, .valueSize = sizeof(CTLineBreakMode), .value = (const void *) &_lineBreakMode},
        {.spec = kCTParagraphStyleSpecifierAlignment, .valueSize = sizeof(CTTextAlignment), .value = (const void *) &_textAlignment}
    };
    CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(paragraphStyles, 2);

    NSDictionary *attrDict = [[NSDictionary alloc] initWithObjectsAndKeys:(id)fontRef, (NSString *)kCTFontAttributeName, (id)_textColor.CGColor, (NSString *)(kCTForegroundColorAttributeName), (id)_strokeColor.CGColor, (NSString *)(kCTStrokeColorAttributeName), (id)[NSNumber numberWithFloat: _strokeWidth], (NSString *)(kCTStrokeWidthAttributeName), (id)paragraphStyle, (NSString *)(kCTParagraphStyleAttributeName), nil];

    CFRelease(fontRef);
    CFRelease(paragraphStyle);

    NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:_text attributes: attrDict];

    // Determine suggested frame height
    CFRange textRange = CFRangeMake(0, [attrStr length]);
    CGSize constraint = CGSizeMake(width, 9999);

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
    CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, textRange, NULL, constraint, NULL);
    textSize = CGSizeMake(ceilf(textSize.width), ceilf(textSize.height));

    [attrDict release];
    [attrStr release];

    return textSize.height;
}

-(void) renderText:(CGContextRef)ctx {
    CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
    CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
    CGContextScaleCTM(ctx, 1.0, -1.0);

    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)_font, _fontSize, NULL);

    CTParagraphStyleSetting paragraphStyles[2] = {
        {.spec = kCTParagraphStyleSpecifierLineBreakMode, .valueSize = sizeof(CTLineBreakMode), .value = (const void *) &_lineBreakMode},
        {.spec = kCTParagraphStyleSpecifierAlignment, .valueSize = sizeof(CTTextAlignment), .value = (const void *) &_textAlignment}
    };
    CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(paragraphStyles, 2);

    NSDictionary *attrDict = [[NSDictionary alloc] initWithObjectsAndKeys:(id)fontRef, (NSString *)kCTFontAttributeName, (id)_textColor.CGColor, (NSString *)(kCTForegroundColorAttributeName), (id)_strokeColor.CGColor, (NSString *)(kCTStrokeColorAttributeName), (id)[NSNumber numberWithFloat: _strokeWidth], (NSString *)(kCTStrokeWidthAttributeName), (id)paragraphStyle, (NSString *)(kCTParagraphStyleAttributeName), nil];

    CFRelease(fontRef);
    CFRelease(paragraphStyle);

    NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:_text attributes: attrDict];

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);

    CFRange textRange = CFRangeMake(0, [attrStr length]);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL);
    CFArrayRef lines = CTFrameGetLines(frame);
    NSInteger numberOfLines = CFArrayGetCount(lines);
    CGPoint lineOrigins[numberOfLines];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);

    for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
        CGPoint lineOrigin = lineOrigins[lineIndex];
        CGContextSetTextPosition(ctx, lineOrigin.x,  lineOrigin.y);
        CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);

        if (lineIndex == numberOfLines - 1) {
            CFRange lastLineRange = CTLineGetStringRange(line);

            if (!(lastLineRange.length == 0 && lastLineRange.location == 0) && lastLineRange.location + lastLineRange.length < textRange.location + textRange.length) {
                NSUInteger truncationAttributePosition = lastLineRange.location;
                CTLineTruncationType truncationType;
                if (numberOfLines != 1) {
                    truncationType = kCTLineTruncationEnd;
                    truncationAttributePosition += (lastLineRange.length - 1);
                }

                NSAttributedString *tokenString = [[NSAttributedString alloc] initWithString:@"\u2026" attributes:attrDict];
                CTLineRef truncationToken = CTLineCreateWithAttributedString((CFAttributedStringRef)tokenString);

                NSMutableAttributedString *truncationString = [[attrStr attributedSubstringFromRange: NSMakeRange(lastLineRange.location, lastLineRange.length)] mutableCopy];
                if (lastLineRange.length > 0) {
                    unichar lastCharacter = [[truncationString string] characterAtIndex: lastLineRange.length - 1];
                    if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
                        [truncationString deleteCharactersInRange:NSMakeRange(lastLineRange.length - 1, 1)];
                    }
                }
                [truncationString appendAttributedString: tokenString];
                CTLineRef truncationLine = CTLineCreateWithAttributedString((CFAttributedStringRef) truncationString);

                CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, self.bounds.size.width, truncationType, truncationToken);
                if (!truncatedLine) {
                    // If the line is not as wide as the truncationToken, truncatedLine is NULL
                    truncatedLine = CFRetain(truncationToken);
                }

                CTLineDraw(truncatedLine, ctx);

                CFRelease(truncatedLine);
                CFRelease(truncationLine);
                CFRelease(truncationToken);
            } else {
                CTLineDraw(line, ctx);
            }
        } else {
            CTLineDraw(line, ctx);
        }
    }

    [attrStr release];
    [attrDict release];

    CFRelease(path);
    CFRelease(frame);
    CFRelease(framesetter);

}

-(void) drawInContext:(CGContextRef)ctx {
    [super drawInContext: ctx];
    [self renderText: ctx];
}

@end
person Dan    schedule 09.10.2012

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

Вы можете центрировать свой кадр, как вы это делали с разными кадрами для разных версий ios.

person DivineDesert    schedule 24.09.2012
comment
Хотя это правильный ответ и определенно будет работать, я надеюсь увидеть более надежный способ решения этой проблемы. Кондиционирование в моей книге довольно хакерское. Спасибо за ваши усилия. - person Byte; 24.09.2012

Мне кажется, что iOS 6 приняла во внимание высоту строки (или другие функции, связанные со шрифтом, которые влияют на фактическое положение глифа по вертикали) шрифта при рисовании текстового содержимого CATextLayer. В результате в iOS 6.0 текст с определенным шрифтом в CATextLayer не отображается на верхнем краю фрейма CATextLayer. Я обнаружил, что у некоторых шрифтов есть такие вертикальные отступы, а у других нет. В то время как в iOS 5.0/5.1 глиф текста фактически отображается на верхнем краю фрейма CATextLayer.

Поэтому я думаю, что одним из возможных решений может быть изменение объекта textLayer в вашем коде с CATextLayer на просто CALayer (или подкласс CALayer) и использование основного текста для пользовательского рисования содержимого, чтобы вы могли контролировать все, что будет согласовано через iOS 5.0/5.1 и 6.0.

person Steve Shi    schedule 27.09.2012