drawRect drawing 'transparent' text?

2019-01-16 17:29发布

问题:

I am looking to draw a UILabel (preferable through subclassing) as a transparent label, but with solid background. I draw up an quick example (sorry, it's ugly, but it gets the points across :)).

Basically I have a UILabel and I would like the background to be a set colour, and the text should be transparent. I do not want to colour the text with the views background, but instead have it be 100% transparent, since I have a texture in the background that I want to make sure lines up inside and outside of the label.

I've been spending the night browsing SO and searching on Google, but I have found no helpful sources. I don't have much experience with CG drawing, so I would appreciate any links, help, tutorial or sample code (maybe Apple has some I need to have a look at?).

Thanks a bunch!

回答1:

I've rewritten it as a UILabel subclass using barely any code and posted it on GitHub

The gist of it is you override drawRect but call [super drawRect:rect] to let the UILabel render as normal. Using a white label color lets you easily use the label itself as a mask.

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    // let the superclass draw the label normally
    [super drawRect:rect];

    CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, CGRectGetHeight(rect)));

    // create a mask from the normally rendered text
    CGImageRef image = CGBitmapContextCreateImage(context);
    CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(image), CGImageGetHeight(image), CGImageGetBitsPerComponent(image), CGImageGetBitsPerPixel(image), CGImageGetBytesPerRow(image), CGImageGetDataProvider(image), CGImageGetDecode(image), CGImageGetShouldInterpolate(image));

    CFRelease(image); image = NULL;

    // wipe the slate clean
    CGContextClearRect(context, rect);

    CGContextSaveGState(context);
    CGContextClipToMask(context, rect, mask);

    CFRelease(mask);  mask = NULL;

    [self RS_drawBackgroundInRect:rect];

    CGContextRestoreGState(context);

}


回答2:

Solved using CALayer masks. Creating a standard mask (wallpapered text, for example) is simple. To create the knocked-out text, I had to invert the alpha channel of my mask, which involved rendering a label to a CGImageRef and then doing some pixel-pushing.

Sample application is available here: https://github.com/robinsenior/RSMaskedLabel

Relevant code is here to avoid future link-rot:

#import "RSMaskedLabel.h"
#import <QuartzCore/QuartzCore.h>

@interface UIImage (RSAdditions)
+ (UIImage *) imageWithView:(UIView *)view;
- (UIImage *) invertAlpha;
@end

@interface RSMaskedLabel ()
{
    CGImageRef invertedAlphaImage;
}
@property (nonatomic, retain) UILabel *knockoutLabel;
@property (nonatomic, retain) CALayer *textLayer;
- (void) RS_commonInit;
@end

@implementation RSMaskedLabel
@synthesize knockoutLabel, textLayer;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) 
    {
        [self RS_commonInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) 
    {
        [self RS_commonInit];
    }
    return self;
}

+ (Class)layerClass
{
    return [CAGradientLayer class];
}

- (void) RS_commonInit
{
    [self setBackgroundColor:[UIColor clearColor]];

    // create the UILabel for the text
    knockoutLabel = [[UILabel alloc] initWithFrame:[self frame]];
    [knockoutLabel setText:@"booyah"];
    [knockoutLabel setTextAlignment:UITextAlignmentCenter];
    [knockoutLabel setFont:[UIFont boldSystemFontOfSize:72.0]];
    [knockoutLabel setNumberOfLines:1];
    [knockoutLabel setBackgroundColor:[UIColor clearColor]];
    [knockoutLabel setTextColor:[UIColor whiteColor]];

    // create our filled area (in this case a gradient)
    NSArray *colors = [[NSArray arrayWithObjects:
                        (id)[[UIColor colorWithRed:0.349 green:0.365 blue:0.376 alpha:1.000] CGColor],
                        (id)[[UIColor colorWithRed:0.455 green:0.490 blue:0.518 alpha:1.000] CGColor],
                        (id)[[UIColor colorWithRed:0.412 green:0.427 blue:0.439 alpha:1.000] CGColor],
                        (id)[[UIColor colorWithRed:0.208 green:0.224 blue:0.235 alpha:1.000] CGColor],
                        nil] retain];

    NSArray *gradientLocations = [NSArray arrayWithObjects:
                                  [NSNumber numberWithFloat:0.0],
                                  [NSNumber numberWithFloat:0.54],
                                  [NSNumber numberWithFloat:0.55],
                                  [NSNumber numberWithFloat:1], nil];

    // render our label to a UIImage
    // if you remove the call to invertAlpha it will mask the text
    invertedAlphaImage = [[[UIImage imageWithView:knockoutLabel] invertAlpha] CGImage];

    // create a new CALayer to use as the mask
    textLayer = [CALayer layer];
    // stick the image in the layer
    [textLayer setContents:(id)invertedAlphaImage];

    // create a nice gradient layer to use as our fill
    CAGradientLayer *gradientLayer = (CAGradientLayer *)[self layer];

    [gradientLayer setBackgroundColor:[[UIColor clearColor] CGColor]];
    [gradientLayer setColors: colors];
    [gradientLayer setLocations:gradientLocations];
    [gradientLayer setStartPoint:CGPointMake(0.0, 0.0)];
    [gradientLayer setEndPoint:CGPointMake(0.0, 1.0)];
    [gradientLayer setCornerRadius:10];

    // mask the text layer onto our gradient
    [gradientLayer setMask:textLayer];
}

- (void)layoutSubviews
{
    // resize the text layer
    [textLayer setFrame:[self bounds]];
}

- (void)dealloc 
{
    CGImageRelease(invertedAlphaImage);
    [knockoutLabel release];
    [textLayer     release];
    [super         dealloc];
}

@end

@implementation UIImage (RSAdditions)

/*
 create a UIImage from a UIView
 */
+ (UIImage *) imageWithView:(UIView *)view
{
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0.0);
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];

    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}

/*
 get the image to invert its alpha channel
 */
- (UIImage *)invertAlpha
{
    // scale is needed for retina devices
    CGFloat scale = [self scale];
    CGSize size = self.size;
    int width = size.width * scale;
    int height = size.height * scale;

    CGColorSpaceRef colourSpace = CGColorSpaceCreateDeviceRGB();

    unsigned char *memoryPool = (unsigned char *)calloc(width*height*4, 1);

    CGContextRef context = CGBitmapContextCreate(memoryPool, width, height, 8, width * 4, colourSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast);

    CGColorSpaceRelease(colourSpace);

    CGContextDrawImage(context, CGRectMake(0, 0, width, height), [self CGImage]);

    for(int y = 0; y < height; y++)
    {
        unsigned char *linePointer = &memoryPool[y * width * 4];

        for(int x = 0; x < width; x++)
        {
            linePointer[3] = 255-linePointer[3];
            linePointer += 4;
        }
    }

    // get a CG image from the context, wrap that into a
    CGImageRef cgImage = CGBitmapContextCreateImage(context);
    UIImage *returnImage = [UIImage imageWithCGImage:cgImage scale:scale orientation:UIImageOrientationUp];

    // clean up
    CGImageRelease(cgImage);
    CGContextRelease(context);
    free(memoryPool);

    // and return
    return returnImage;
}
@end


回答3:

Here's a technique that's similar to Matt Gallagher's, which will generate an inverted text mask with an image.

Allocate a (mutable) data buffer. Create a bitmap context with an 8-bit alpha channel. Configure settings for text drawing. Fill the whole buffer in copy mode (default colour assumed to have alpha value of 1). Write the text in clear mode (alpha value of 0). Create an image from the bitmap context. Use the bitmap as a mask to make a new image from the source image. Create a new UIImage and clean up.

Every time the textString or sourceImage or size values change, re-generate the final image.

CGSize size = /* assume this exists */;
UIImage *sourceImage = /* assume this exists */;
NSString *textString = /* assume this exists */;
char *text = [textString cStringUsingEncoding:NSMacOSRomanStringEncoding];
NSUInteger len = [textString lengthOfBytesUsingEncoding:cStringUsingEncoding:NSMacOSRomanStringEncoding];

NSMutableData *data = [NSMutableData dataWithLength:size.width*size.height*1];
CGContextRef context = CGBitmapContextCreate([data mutableBytes], size.width, size.height, 8, size.width, NULL, kCGImageAlphaOnly);

CGContextSelectFont(context, "Gill Sans Bold", 64.0f, kCGEncodingMacRoman);
CGContextSetTextDrawingMode(context, kCGTextFill);

CGContextSetBlendMode(context, kCGBlendModeCopy);
CGContextFillRect(context, overlay.bounds);
CGContextSetBlendMode(context, kCGBlendModeClear);
CGContextShowTextAtPoint(context, 16.0f, 16.0f, text, len);

CGImageRef textImage = CGBitmapContextCreateImage(context);
CGImageRef newImage = CGImageCreateWithMask(sourceImage.CGImage, textImage);

UIImage *finalImage = [UIImage imageWithCGImage:newImage];

CGContextRelease(context);
CFRelease(newImage);
CFRelease(textImage);

Another way to do this involves putting the textImage into a new layer and setting that layer on your view's layer. (Remove the lines that create "newImage" and "finalImage".) Assuming this happens inside your view's code somewhere:

CALayer *maskLayer = [[CALayer alloc] init];
CGPoint position = CGPointZero;

// layout the new layer
position = overlay.layer.position;
position.y *= 0.5f;
maskLayer.bounds = overlay.layer.bounds;
maskLayer.position = position;
maskLayer.contents = (__bridge id)textImage;

self.layer.mask = maskLayer;

There are more alternatives, some might be better (subclass UIImage and draw the text directly in clear mode after the superclass has done its drawing?).