Question

I have some troubles with CATextLayer, that could be due to me, but I didn't find any help on this topic. I am on OS X (on iOS it should be the same).

I create a CATextLayer layers with scale factor > 1 and what I get is a blurred text. The layer is rasterized before applying the scale, I think. Is this the expected behavior? I hope it is not, because it just makes no sense... A CAShapeLayer is rasterized after that its transformation matrix is applied, why the CATextLayer should be different?

In case I am doing something wrong... what is it??

CATextLayer *layer = [CATextLayer layer];
layer.string = @"I like what I am doing";
layer.font = (__bridge CFTypeRef)[NSFont systemFontOfSize:24];
layer.fontSize = 24;
layer.anchorPoint = CGPointZero;
layer.frame = CGRectMake(0, 0, 400, 100);
layer.foregroundColor = [NSColor blackColor].CGColor;
layer.transform = CATransform3DMakeScale(2., 2., 1.);
layer.shouldRasterize = NO;
[self.layer addSublayer:layer];

The solution I use at the moment is to set the contentsScale property of the layer to the scale factor. The problem is that this solution doesn't scale: if the scale factor of any of the parent layers changes, then contentsScale should be updated too. I should write code to traverse the layers tree to update the contentsScale properties of all CATextLayers... not exactly what I would like to do.

Another solution, that is not really a solution, is to convert the text to a shape and use a CAShapeLayer. But then I don't see the point of having CATextLayers.

A custom subclass of CALayer could help in solving this problem?

EDIT: Even CAGradientLayer is able to render its contents, like CAShapeLayer, after that its transformation matrix is applied. Can someone explain how it is possible?

EDIT 2: My guess is that paths and gradients are rendered as OpenGL display lists, so they are rasterized at the actual size on the screen by OpenGL itself. Texts are rasterized by Core Animation, so they are bitmaps for OpenGL.

I think that I will go with the contentsScale solution for the moment. Maybe, in the future, I will convert texts to shapes. In order to get best results with little work, this is the code I use now:

[CATransaction setDisableActions:YES];

CGFloat contentsScale = ceilf(scaleOfParentLayer);
// _scalableTextLayer is a CATextLayer
_scalableTextLayer.contentsScale = contentsScale;
[_scalableTextLayer displayIfNeeded];

[CATransaction setDisableActions:NO];
Was it helpful?

Solution

After trying all the approaches, the solution I am using now is a custom subclass of CALayer. I don't use CATextLayer at all.

I override the contentsScale property with this custom setter method:

- (void)setContentsScale:(CGFloat)cs
{
    CGFloat scale = MAX(ceilf(cs), 1.); // never less than 1, always integer
    if (scale != self.contentsScale) {
        [super setContentsScale:scale];
        [self setNeedsDisplay];
    }
}

The value of the property is always rounded to the upper integer value. When the rounded value changes, then the layer must be redrawn.

The display method of my CALayer subclass creates a bitmap image of the size of the text multiplied by the contentsScale factor and by the screen scale factor.

- (void)display
{
    CGFloat scale = self.contentsScale * [MyUtils screenScale];

    CGFloat width = self.bounds.size.width * scale;
    CGFloat height = self.bounds.size.height * scale;

    CGContextRef bitmapContext = [MyUtils createBitmapContextWithSize:CGSizeMake(width, height)];

    CGContextScaleCTM(bitmapContext, scale, scale);
    CGContextSetShouldSmoothFonts(bitmapContext, 0);

    CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)(_text));

    CGContextSetTextPosition(bitmapContext, 0., self.bounds.size.height-_ascender);
    CTLineDraw(line, bitmapContext);
    CFRelease(line);

    CGImageRef image = CGBitmapContextCreateImage(bitmapContext);
    self.contents = (__bridge id)(image);
    CGImageRelease(image);

    CGContextRelease(bitmapContext);
}

When I change the scale factor of the root layer of my hierarchy, I loop on all text layers and set the contentsScale property to the same factor. The display method is called only if the rounded value of the scale factor changes (i.e. if the previous value was 1.6 and now I set 1.7, nothing happens. But if the new value is 2.1, then the layer is redisplayed).

The cost in terms of speed of the redraw is little. My test is to change continuously the scale factor of a hierarchy of 40 text layers on an 3rd gen. iPad. It works like butter.

OTHER TIPS

CATextLayer is different because the underlying CoreText renders the glyphs with the specified font size (educated guess based on experiments).

You could add an action to the parent layer so as soon as it's scale changes, it changes the font size of the text layer.

Blurriness could also come from misaligned pixels. That can happen if you put the text layer to non integral position or any transformation in the superlayer hierarchy.

Alternatively you could subclass CALayer and then draw the text using Cocoa in drawInContext: see example here: http://lists.apple.com/archives/Cocoa-dev/2009/Jan/msg02300.html http://people.omnigroup.com/bungi/TextDrawing-20090129.zip

If you want to have the exact behaviour of a CAShapeLayer then you will need to convert your string into a bezier path and have CAShapeLayer render it. It's a bit of work but then you will have the exact behaviour you are looking for. An alternate approach, is to scale the fontSize instead. This yields crisp text every time but it might not fit to you exact situation.

To draw text as CAShapeLayer have a look at Apple Sample Code "CoreAnimationText": http://developer.apple.com/library/mac/#samplecode/CoreAnimationText/Listings/Readme_txt.html

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top