In my recently released app, Wire Draw (which you can read more about here) I wanted to create a color picker to allow the users to choose the colors of lines. Unfortunately there isn't a color picker control in UIKit. I did find one on line, with full code on how to implement it, but that was a full screen affair and I just wanted something that would fit nicely into the existing settings UI. I finally decided on RGB sliders, which are easy enough to implement, but not very visual. I wanted the user to know instantly which was the red, green, or blue slider. I finally came up with what you see here:
I'm not saying it's perfect, but it worked pretty well for me.
Skinning the sliders wasn't the most obvious thing in the world to do, and I'm not sure I've seen any other apps that do it (I'm sure there are), so I thought I'd share what I learned.
First of all, create your slider. You can create and position it in code, or do it through Interface Builder. If you do the latter, make sure you create an IBOutlet for it and make the connection to that outlet in IB so that your code has access to the slider. All the skinning is done via code. We'll assume your slider is named "slider".
If you select the slider in Interface Builder and look at the Attributes Inspector, you will see two dropdowns for Min Image and Max Image. These allow you to put little pictures to the left or right of the slider. Say you were making a volume control. On the right, at maximum volume, you might want an icon that showed a little speaker with sound waves coming out. On the left, zero volume, you might want a speaker with no sound waves, or maybe even an "x" through it. That's pretty simple, but is not what I'm talking about when I say skinning. Again, to skin the actual slider itself, you need to leave IB and write some code.
The methods that affect the appearance of the slider are:
C:
- setThumbImage:forState:
setMinimumTrackImage:forState:
- setMaximumTrackImage:forState:
Let's start with the thumb. This is the button that you press and move back and forth. Create a new image to use for this. I recommend you use a transparent png file, around 24x24 pixels. Here's what I made for my red slider:
Add that to your projects. Then add the following line of code, somewhere where it will run early and once, such as viewDidLoad of the view controller where the slider is located.
C:
- [slider setThumbImage:[UIImage imageNamed:@"redThumb.png"]forState:UIControlStateNormal];
This tells the slider to use the specified image as the thumb in the normal state. Actually, if you only specify the normal state, it will be used for all states. If that works for you, that's all you have to do. If you want a different thumb to show when the user is pressing it (not that they'll be able to see much of it with their finger over it), add another line with the other image and UIControlStateHighlighted for the state. You can also specify another image for UIControlStateDisabled. There are other UI Control States, but I'm not sure any of the others apply to a slider, so those are the three you'll most likely be using.
But say we just stick to the normal state as above. We run the app and we get this:
Hmmm... well, thumb skinning was successful, but we lost the rest of the slider! It seems that slider skinning is an all or nothing proposition. So let's get to work on the tracks. As you saw, we have methods to set the minimum and maximum track images. But what does that mean exactly?
Simply put, the minimum track image is the image of the track that appears to the left of the thumb, and the maximum track image is what appears to the right of the thumb. Again, these have states that work just like the thumb image. Let's just use normal for now, which covers all states.
I made a red track image which looks like this in it's raw form:
I also made a black one you can see here:
The red track will be the minimum image and the black track will be the maximum image, which will give you the effect you can see in the final screenshot at the beginning of this post. But you'll notice that these track images are very small. That's fine because they will be stretched to fill the entire space from the edge of the slider to the thumb. This is great if your track image is a solid fill rectangle with no shadows or anything. But let's see what happens if it's not, like the ones we are using. You can already probably figure out how to apply these images, but I'll give you the code anyway:
C:
- [slider setThumbImage:[UIImage imageNamed:@"redThumb.png"]forState:UIControlStateNormal];
[slider setMinimumTrackImage:[UIImage imageNamed:@"redTrack.png"]forState:UIControlStateNormal];
- [slider setMaximumTrackImage:[UIImage imageNamed:@"blackTrack.png"]forState:UIControlStateNormal];
Run that and we get this:
Ouch. Yeah, it stretched them all right, along with the curved corners and shadows. Now, if you've come from the Flash world, you're thinking, "Scale9!!! Use Scale9!" And right you are! There is an distant cousin of Scale9 in UIKit, called a "stretchable image". You can create a stretchable image by taking a regular UIImage and calling the method, "stretchableImageWithLeftCapWidth:topCapHeight:" on it. This creates a new UIImage which can be stretched while not distorting the edges and corners, just like Scale9 in Flash. However, I said it's a distant cousin, and it really is quite distant. Although they has the same end effect, stretchable images are defined quite differently. As you see, we only pass in two parameters: leftCapWidth and topCapHeight. What about the right and bottom? Well, those are kind of dynamic. Here's how it works:
The leftCapWidth is the margin on the left side of the image that will not be stretched. The right cap width (although there is no actual variable named that) is the remaining width, minus one pixel. Alright, that's not very clear. We need a drawing.
Ignoring the topCapHeight for now (which you can do by setting it to zero), we see we have an image that is 21 pixels wide. If we set the leftCapWidth to 10, that means the first 10 pixels will not be stretched. What will be stretched is the next single pixel. Only that pixel and nothing more, and it's always just one pixel. Finally, everything to the right of that single pixel will not be stretched. So, in the above example, the image is 21 pixels wide. The leftCapWidth is 10, then there is one stretchy pixel. That leaves the right 10 pixels as not stretchable, which would be your right cap width, if such a variable existed.
Note that while my example is symmetrical, it doesn't have to be. If I had made the leftCapWidth 5, then the right portion would have been 15 (21 - 5 - 1). Or if the whole image was only 20 pixels and leftCapWidth 10, the right portion would be 9 (20 - 10 - 1). Again, the stretchable area is always 1 pixel, so I don't think it's too easy to stretch a gradient like you can do in Scale9 in Flash (not that it usually works out too well anyway). If you are into formulas, the width of the right portion is total width - leftCapWidth - 1.
The topCapHeight works exactly the same way, but we don't need it here. Let's put this into action. Rather than massively nested brackets, we'll create a reference to the two stretchable images.
C:
- [slider setThumbImage:[UIImage imageNamed:@"redThumb.png"]forState:UIControlStateNormal];
- UIImage *minTrackImage = [[UIImage imageNamed:@"redTrack.png"]stretchableImageWithLeftCapWidth:10 topCapHeight:0];
[slider setMinimumTrackImage:minTrackImage forState:UIControlStateNormal];
-
UIImage *maxTrackImage = [[UIImage imageNamed:@"blackTrack.png"]stretchableImageWithLeftCapWidth:10 topCapHeight:0];
- [slider setMaximumTrackImage:maxTrackImage forState:UIControlStateNormal];
And that give you this:
Yay!
Note that in this case, I could have made the right edge of the red track perfectly square, and chopped it off at 11 pixels. This would mean that the first 10 pixels would not stretch, the next (and final) pixel would stretch the rest of the distance, and nothing would be left over on the right side. Either way would work here, because the right edge of the track is hidden under the thumb. The inverse could be done for the black track.
Well, that's all folks. Hope this makes a short part of your day a bit easier some time in the future.