The UIView contentStretch property defines an area inside of which content is stretched to fill the view. That's great and all, but I find myself in need of the exact opposite: I want a view where I can define a rect that does not stretch but have the outer edges stretched until they fill the view.
+-------------------------------------------+
| |
| +----------+ |
| | | |
| | | |
| | Content | Stretch to fill |
| | | |
| | | |
| +----------+ |
| |
+-------------------------------------------+
So in the above view, the outer rect is my proposed view. The inner rect is the non-stretchable content. Ideally I would like to be able to position the center point of the non-stretchable content anywhere within the outer rect and still have the outer bits fill to the edge.
Example usage scenario: a black overlay with a transparent center "hole" that follows the mouse / touch for exploring a scene with a flashlight, etc.
One way I thought to do this is to draw the content UIView, then draw four other views, size appropriately, to cover the outer area, but I was hoping for a single-view solution for smoother animating. I'm guessing I'll need to have a single UIView and use Core Animation to get down and mess with its Layers?
Presumably you wouldn't need four other views, you'd need eight. The inner “fixed” rectangle divides the view into nine slices:
Notice that each of the nine slices may require a different combination of horizontal and vertical scaling. It ends up looking like this:
Anyway, I thought it wouldn't be too hard to do this just by creating a
UIView
subclass that overridesdrawRect:
.I named my subclass
OutstretchView
and gave it three properties:The
fixedRect
property defines the part of the image that should never be stretched. ThefixedCenter
property defines where in the view that part of the image should be drawn.To actually draw the nine slices of the image, it is helpful to define a method that takes a
CGRect
in image coordinates and aCGRect
in view coordinates, and draws the specified part of the image in the specified part of the view. I use Quartz 2D's clip and CTM features to do the “heavy lifting”:I'll also use a little helper method that creates a
CGRect
from an array of two X coordinates and an array of two Y coordinates:With those helpers, I can implement
drawRect:
. First I handle the easy cases where there is either no image to draw, or no definedfixedRect
:Then I compute the rectangle in view coordinates where the fixed part of the image will be drawn:
Next I make an array of the four (yes, four) X coordinates that define the edges of the slices in view coordinate space, and a similar array for the Y coordinates, and two more arrays for the coordinates in image coordinate space:
And finally the easy part, drawing the slices:
It's a little laggy on my iPad 2.
This version of my test project is on github.
So my first attempt (in my other answer to this question), doing everything in
drawRect:
, was a little laggy on my iPad 2. I decided to redo it by creating a separateCALayer
for each slice and let Core Animation worry about scaling the slices.In this version, the work is done in the
layoutSubviews
method of my customUIView
subclass. The system callslayoutSubviews
whenever the view's size changes (including when the view first appears). We can also ask the system to calllayoutSubviews
using thesetNeedsLayout
message. The system automatically coalesces layout requests like it coalesces draw requests.I'll need three private instance variables:
My
layoutSubviews
method starts by handling the easy cases of no image or nofixedRect
:Then it lazily creates the nine slice layers if I haven't created them yet:
It recomputes the images for the slice layers if the image has changed. The
_imageDidChange
flag is set insetImage:
.Finally, it sets the frame of each of the nine slice layers.
Of course, all the real work happens in the helper methods, but they're pretty simple. Hiding the slices (when there's no image or no
fixedRect
) is trivial:Creating the slice layers is also trivial:
To make the slice images and set the slice layer frames, I'll need the
rect
helper function from my other answer:Creating the slice images requires a little work, because I have to compute the coordinates of the boundaries between the slices. I use
CGImageCreateWithImageInRect
to create the slice images from the user-supplied image.Setting the slice layer frames is similar, although I don't have to handle the image scale here. (Maybe I should be using the
UIScreen
scale? Hmm. It's confusing. I haven't tried it on a Retina device.)This version seems much smoother on my iPad 2. It might work very well on older devices too.
This version of my test project is on github too.