Unwanted Vertical Padding from iOS 6 on CATextLayer

前端 未结 5 1030
半阙折子戏
半阙折子戏 2021-02-06 15:37

Background: I started my project in iOS 5 and built out a beautiful button with layer. I added a textLayer onto the button and center it using the following cod

相关标签:
5条回答
  • 2021-02-06 15:47

    In iOS 5 and before, the first baseline in a CATextLayer is always positioned down from the top of the bounds by the ascent obtained from CTLineGetTypographicBounds when passed a CTLine made with the string for the first line.

    In iOS 6, this doesn't hold true for all fonts anymore. Hence, when you are positioning a CATextLayer you can no longer reliably decide where to put it to get the right visual alignment. Or can you? ...

    First, an aside: when trying to work out CATextLayer's positioning behaviour a while ago in iOS 5, I tried using all combinations of cap height, ascender from UIFont, etc. before finally discovering that ascent from CTLineGetTypographicBounds was the one I needed. In the process, I discovered that a) the ascent from UIFont ascender, CTFontGetAscent and CTLineGetTypographicBounds are inconsistent for certain typefaces, and b) the ascent is frequently strange - either cropping the accents or leaving way to much space above. The solution to a) is to know which value to use. There isn't really a solution to b) other than to leave plenty of room above by offsetting CATextLayer bounds if it likely you will have accents that get clipped.

    Back to iOS 6. If you avoid the worst offending typefaces (as of 6.0, and probably subject to change), you can still do programatic positioning of CATextLayer with the rest of the typefaces. The offenders are: AcademyEngravedLetPlain, Courier, HoeflerText and Palatino - visually, these families position correctly (i.e. without clipping) in CATextLayer, but none of the three ascent sources gives you a usable indication of where the baseline is placed. Helvetica and .HelveticaNeueUI (aka system font) families position correctly with baseline at the ascent given by UIFont ascender, but the other ascent sources are not of use.

    Some examples from tests I did. The sample text is drawn three times in different colours. The coordinate origin is top left of grey box. Black text is drawn by CTLineDraw offset downwards by the ascent from CTLineGetTypographicBounds; transparent red is drawn by CATextLayer with bounds equal to the grey box; transparent blue is drawn with the UIKit NSString addition drawAtPoint:withFont: locating at the origin of the grey box and with the UIFont.

    1) A well behaved font, Copperplate-Light. The three samples are coincident, giving maroon, and meaning that the ascents are near enough the same from all sources. Same for iOS 5 and 6.

    copperplate-light iOS 6

    2) Courier under iOS 5. CATextLayer positions text too high (red), but CTLineDraw with ascent from CTLineGetTypographicBounds (black) matches CATextLayer positioning - so we can place and correct from there. NSString drawAtPoint:withFont: (blue) places the text without clipping. (Helvetica and .HelveticaNeueUI behave like this in iOS 6)

    courier iOS 5

    3) Courier under iOS 6. CATextLayer (red) now places the text so that it is not clipped, but the positioning no longer matches the ascent from CTLineGetTypographicBounds (black) or from UIFont ascender used in NSString drawAtPoint:withFont: (blue). This is unusable for programatic positioning. (AcademyEngravedLetPlain, HoeflerText and Palatino also behave like this in iOS 6)

    courier iOS 6

    Hope this helps avoid some of the hours of wasted time I went through, and if you want to dip in a bit deeper, have a play with this:

    - (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;
    }
    
    0 讨论(0)
  • 2021-02-06 15:58

    I think to support both you can create a category for text layers, in category you can code it conditionally for both versions. Same as we do for navigation bar when we change image.

    You can center your frame as you did with different frames for different ios versions

    0 讨论(0)
  • 2021-02-06 16:00

    Wile I am waiting for an ultimate solution, I studied about RTLabel and TTTAttributedLabel, and made a simple class to draw text on a CALayer as Steve suggested. Hope it helps, and please don't hesitant to point out any mistake I have made.

    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
    
    0 讨论(0)
  • 2021-02-06 16:04

    t0rst's answer helps me. I think capHeight and xHeight are key.

        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);
    
    0 讨论(0)
  • 2021-02-06 16:05

    It seems to me that iOS 6 has taken into account the Line Height (or other font related features that affects the actual vertical drawing position of the glyph) of the font when drawing the text contents of CATextLayer. The result is that in iOS 6.0, the text with certain font in CATextLayer is not displayed at the top edge of the frame of the CATextLayer. I found that some font has such vertical padding while others don't. While in iOS 5.0/5.1, the glyph of the text is actually displayed at the top edge of the frame of the CATextLayer.

    So one possible solution I'm thinking may be to change the textLayer object in your code from CATextLayer to just CALayer (or subclass CALayer) and use Core Text to custom draw the contents such that you get to control of everything that will be consistent across iOS 5.0/5.1 and 6.0.

    0 讨论(0)
提交回复
热议问题