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:
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:
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.
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).
Next clip to the original image and draw the inverted image with a shadow,
this will make the inside shadow.
Finally clear up, and set the result so that it is drawn to screen.
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:
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:
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):
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