« Older: Inner Shadow Drawing « Older
Newer: Trains »Newer »
Automatic Inner Shadows
The last post was about drawing inner shadows on the inside of a known shape to give the effect that the shape was embossed, or indented. The approach taken was quite a lot of work, and only works for known shapes; we couldn’t for instance indent an image or letters with this approach. We also need to write new code for any shape that we wish to indent. It would be nice to have an object that would do all of this work for us for any given shape.
To automatically create inner shadows we will in some form have to
do our own drawing. We can do this by making a subclass of CALayer.
The way drawing works on a CALayer is you call setNeedsDisplay
on
your layer, this adds an item to the runloop to call display
on
your layer the next time round the run loop. Any subsequent calls
to setNeedsDisplay won’t do anything until display
has been
called by the runloop. display
works by calling your drawing code
and caching the result in the layers’ content, then display
won’t
be called again unless the setNeedsDisplay
method is called.
To do custom drawing we can do all of this ourselves with
our own customisations.
The first thing that we are going to do in our display
method is to
create a CGContextRef
we can then pass this to either the delegates
drawLayer:inContext:
, or the layers’ drawInContext:
method which
will do the normal drawing. Here’s the code:
CGRect bounds = [self bounds];
CGFloat width = ceilf (bounds.size.width);
CGFloat height = ceilf (bounds.size.height);
uint8_t *dataBytes = malloc (width * height * sizeof(uint32_t));
CGFloat bytesPerDrawingRow = width * 4;
CGFloat bytesPerDrawingComponent = 4;
CGColorSpaceRef rgbSpace = CGColorSpaceCreateDeviceRGB ();
CGContextRef bitmapCtx = CGBitmapContextCreate (dataBytes,
width,
height,
8,
bytesPerDrawingRow,
rgbSpace,
kCGImageAlphaPremultipliedLast);
To make this work for a retina device you will need twice as many pixels in each direction and to set a transform to make the drawing happen at the right size.
Next we need to call the appropriate drawing method:
id delegate = [self delegate];
CGImageRef mask;
CGContextSaveGState (bitmapCtx);
if (delegate && [delegate respondsToSelector:@selector (displayLayer:)]) {
[delegate displayLayer:self];
id content = [self contents];
if ([content isKindOfClass:[UIImage class]])
mask = (CGImageRef)CFRetain ((__bridge CGImageRef)[self contents]);
else {
// CGLayers save things in a backing form that isn't documented normally.
CFRelease (rgbSpace);
CFRelease (bitmapCtx);
free (dataBytes);
[super display];
return;
}
} else {
if (delegate && [delegate respondsToSelector:@selector(drawLayer:inContext:)])
[delegate drawLayer:self inContext:bitmapCtx];
else
[self drawInContext:bitmapCtx];
mask = CGBitmapContextCreateImage (bitmapCtx);
}
CGContextRestoreGState (bitmapCtx);
If at this point we called setContents:mask
then we would have effectively
re-implemented the original display
method. Probably less efficiently, with
some memory leaks!
We can now manipulate what has been drawn to create the shadows that we
to draw. To draw the outside shadow we will draw mask
with the context
set to draw the correct shadow. To draw the inside shadow we invert the
alpha channel of image data, clip to mask
, set up the shadows that
we want, and draw the inverted alpha image.
First we will create the image with the inverted alpha channel. We have all
the bitmap data of the drawn image in dataBytes
, so we can fiddle with
this directly.
union dataUnion {
uint32_t intVal;
uint8_t byteVals[4];
};
union dataUnion baseData;
baseData.byteVals[0] = 0.0;
baseData.byteVals[1] = 0.0;
baseData.byteVals[2] = 0.0;
unsigned inverseSize = width * height * sizeof (uint32_t);
uint32_t *shadowImageData = malloc(inverseSize);
// Create the inverse mask. This does a lot of operations, possibly could be
// done with OpenGL shader, or using the Accelerate framewok.
for (int row = 0; row < height; ++row) {
for (int column = 0; column < width; ++column) {
if (clipForAnyAlpha) {
if (dataBytes[(int)(row * bytesPerDrawingRow + column * bytesPerDrawingComponent) + 3] > 1)
baseData.byteVals[3] = 0;
else
baseData.byteVals[3] = 255;
} else {
baseData.byteVals[3] = 255 - dataBytes[(int)(row * bytesPerDrawingRow + column * bytesPerDrawingComponent) + 3];
}
shadowImageData[(int)(row * width + column)] = baseData.intVal;
}
}
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL,
shadowImageData,
inverseSize,
dataRelease);
CGImageRef image = CGImageCreate (width,
height,
8,
32,
4 * width,
rgbSpace,
kCGImageAlphaPremultipliedLast,
dataProvider,
NULL,
NO,
kCGRenderingIntentDefault);
CFRelease (dataProvider);
Next we are going to draw the outside shadow. If we want to make it so
that the original image isn’t drawn then we can clip to the inverted image
before we draw the original data (which is in mask
).
CGContextClearRect (bitmapCtx, bounds);
/* Flip the y-axis so that things draw the right way up. */
CGContextConcatCTM (bitmapCtx, CGAffineTransformMake (1.0, 0.0,
0.0, -1.0, 0.0, height));
CGContextSaveGState(bitmapCtx);
if (!drawOriginalImage)
CGContextClipToMask (bitmapCtx, bounds, image);
CGColorRef outsideShadow;
if (!outsideShadowColor) {
CGFloat whiteCol[] = { 1.0, 1.0, 1.0, 0.4 };
outsideShadow = CGColorCreate (rgbSpace, whiteCol);
} else
outsideShadow = (CGColorRef)CFRetain ([outsideShadowColor CGColor]);
CGSize shadowSize = CGSizeMake (0.0, -1.0);
CGFloat radius = 2.0;
if (outsideShadowSize) {
[outsideShadowSize getValue:&shadowSize];
shadowSize = CGSizeMake (shadowSize.width, -shadowSize.height);
}
if (outsideShadowRadius)
radius = [outsideShadowRadius floatValue];
CGContextSetShadowWithColor (bitmapCtx, shadowSize, radius, outsideShadow);
CGContextDrawImage (bitmapCtx, bounds, mask);
CGContextRestoreGState (bitmapCtx);
Next clip to the original image and draw the inverted image with a shadow, this will make the inside shadow.
CGContextSaveGState(bitmapCtx);
CGContextClipToMask (bitmapCtx, bounds, mask);
shadowSize = CGSizeMake (0.0, -2.0);
radius = 2.0;
if (insideShadowSize) {
[insideShadowSize getValue:&shadowSize];
shadowSize = CGSizeMake (shadowSize.width, -shadowSize.height);
}
if (insideShadowRadius)
radius = [insideShadowRadius floatValue];
CGColorRef shadowColor;
if (insideShadowColor)
shadowColor = (CGColorRef)CFRetain ([insideShadowColor CGColor]);
else {
CGFloat defaultVals[] = { 0.0, 0.0, 0.0, 0.75 };
shadowColor = CGColorCreate (rgbSpace, defaultVals);
}
CGContextSetShadowWithColor (bitmapCtx, shadowSize, radius, shadowColor);
CGContextDrawImage (bitmapCtx, bounds, image);
CGContextRestoreGState(bitmapCtx);
Finally clear up, and set the result so that it is drawn to screen.
CFRelease (shadowColor);
CFRelease (mask);
CFRelease (outsideShadow);
CFRelease (image);
CGImageRef result = CGBitmapContextCreateImage (bitmapCtx);
CFRelease (bitmapCtx);
[self setContents:(__bridge id)(result)];
CFRelease (result);
free (dataBytes);
Now for any view that does its drawing using the drawRect:
method all we
need to do to make inner shadows is to subclass the view, and swap its
layer for our own. With what we’ve done if the view does its drawing in a
different way, this layer won’t help us.
Here’s an example of a UILabel subclass changed to indent its content:
#import "JTAIndentLabel.h"
#import "JTAInnerShadowLayer.h"
@implementation JTAIndentLabel
static void commonInit (JTAIndentLabel *self)
{
[self setBackgroundColor:[UIColor clearColor]];
JTAInnerShadowLayer *innerShadow = (JTAInnerShadowLayer *)[self layer];
[innerShadow setClipForAnyAlpha:YES];
[innerShadow setOutsideShadowSize:CGSizeMake(0.0, 1.0) radius:1.0];
[innerShadow setInsideShadowSize:CGSizeMake (0.0, 4.0) radius:6.0];
// Uncomment this to make the label also draw the text (won't work well
// with black text!
// [innerShadow drawOriginalImage];
}
+ (Class)layerClass
{
return [JTAInnerShadowLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
commonInit (self);
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if ((self = [super initWithCoder:aDecoder]))
commonInit (self);
return self;
}
@end
That’s it! here’s the result:
By changing the colour and size of the shadows we can emboss things instead of indenting them; resulting in something like this:
We can also make our own image view like this:
- (id)initWithImage:(UIImage *)i andScale:(CGAffineTransform)scalingTrans
{
if ((self = [super initWithFrame:
CGRectApplyAffineTransform ((CGRect) {
(CGPoint){ 0.0, 0.0 },
[i size]},
scalingTrans)])){
JTAInnerShadowLayer *innerShadow = (JTAInnerShadowLayer *)[self layer];
insideShadSize = CGSizeMake (0.0, 4.0);
outsideShadowSize = CGSizeMake (0.0, 1.0);
[innerShadow setOutsideShadowSize:outsideShadowSize radius:1.0];
[innerShadow setInsideShadowSize:insideShadSize radius:6.0];
//[innerShadow setDrawOriginalImage:YES];
motionManager = [[CMMotionManager alloc] init];
[motionManager setAccelerometerUpdateInterval:1.0 / 60.0];
[motionManager startAccelerometerUpdates];
image = i;
scale = scalingTrans;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext ();
CGContextConcatCTM (ctx, CGAffineTransformMake (1.0, 0.0, 0.0, -1.0,
0.0, rect.size.height));
CGContextDrawImage (ctx, rect, [image CGImage]);
}
Here’s what the results look like:
With this CGLayer subclass we can quickly and very easily indent a lot of things. There are some possible issues with this. We use quite a lot of memory to manipulate the images, and we have to loop over every pixel in the image to invert the alpha channel. This may cause issues if you are doing this a lot, or if you have a very large drawing area.
You can find and use the code for this layer here. If you use this code please give me some credit for it.
Update
I’ve added a delegate method to the CALayer delegate protocol that allows
you to draw whatever you want in the embossed, or indented region. The
method is called drawToshadowedRegionInContext:
. Here’s an example of
using this method (this code is added to your UIView subclass):
- (void)drawToshadowedRegionInContext:(CGContextRef)ctx
{
CGContextDrawImage (ctx, [self bounds],
[[UIImage imageNamed:@"IMG_4229.jpg"] CGImage]);
}
I added this to my emboss view class, and this is the result:
I’ve committed these changes to the git repository, and the source is still
here
Update
Added Support for retina devices.