Positioning stretched UIImages correctly - iphone

I am creating a UISegmentedControl replacement so that it works with your own custom images. Because the separators need different colors on both sides I decided to give the middle item the two borders.
Now to display the UISegmentedControl replacement, I calculate the available width for one item (frame.size.width / numberOfItems). Then I create a UIButton with a custom background image (the stretchable middle segment image). The next thing is to position everything. Because the 1px separators need to be visible when you select an item, I give every item a 1px larger frame than it actually should have. So the next item overlaps 1px to the left.
segmentRect = CGRectMake(indexOfObject * (self.frame.size.width / numberOfSegments), 0, (self.frame.size.width / numberOfSegments) + 1, self.frame.size.height);
Using this result I get a nearly perfect custom UISegmentedControl (retina):
Now things look pretty ok, but it all changes when adding more/less segments. This was a 300px wide control by the way, so each segment gets 25px of space. If I change that number to let's say 13, this shows up:
Notice the slightly different border between the '3' and '4'. (Easier to spot on non-retina actually because of pixel doubling) I think this is caused by the not so nice amount of space each segment gets. (300 / 13 =
23,0769) One should think that the stretchable image would accommodate for this, no? The separators are exactly 1px in width and I change the frame by 1px, so the two separators should be placed exactly on top of each other, which is definitely not the case here.
Does anyone have an explanation why this happens and more important a way to fix this?

All coordinates are floats (CGFloat's, actually) by default, so if you're handing the system non-integer coordinates it will try its best to draw between pixels, using anti-aliasing. The weird 3/4 border is probably due to this.
One solution is to give all your segments integer-sized widths. This may sound a bit nasty, since they might not all be the same width in order to fill up the space (i.e. if the overall width is not divisible by the number of segments), but code like the following shows how this can be done without too much trouble:
int totalWidth = self.frame.size.width;
float dx = (float)totalWidth / numSegments;
float lastX = 0.0;
for (int i = 0; i < numSegments; ++i) {
int thisWidth = round(lastX + dx) - round(lastX);
CGRect segmentFrame = CGRectMake(round(lastX), 0, thisWidth, height);
/* do what you will with segmentFrame */
lastX += dx;
}
I personally like this technique because it automatically evenly distributes the widths that are slightly larger than the others (all the widths will be within 1 pixel of each other).
This code is not including your 1-pixel overlaps, but that is easy to add.
Another benefit is that if your UILabel's or UIImageView's also have integer coordinates (in the coordinate system of the entire screen), they tend to look more crisp.

Yea, my guess is that UIView subclasses don't like uneven frame sizes. I've come across this so many times. What if you try making your segmented view 325px wide? If it fixes the glitch, then you've got your answer :)

I am not sure of the answer, I think it has to be with antialiasing. (Which is something we cannot change in this case).
An approach could be something like:
int x = width%numberOfButtons;
if(x == 0){
//do as now
}else{
//make the first x buttons 1px bigger
}

I managed to come up with something myself. I floor the width of each segment, so the middle buttons will look ok. Then I divide the unused space between the first and last button. (again a rounded value for the first)

Related

UISegmentedControl width in SplitView

I have several UISegmentedControls in different view controllers. On the iPad, when the device changes orientation I realign the segments inside these so they line up with the UITableViews underneath. The problem I'm seeing is that although the resizing mask is set for the UISegmentedControl and without any of my code, it resizes to fill the right width, once I try to change the width of the segments within, the segmented will either not stretch all the way to the end, or they'll be too big and go over.
This only seems to happen when the controls are in a split view.
- (void)orientationChanged:(NSNotification *)notification {
[self setHeaderWidths];
}
-(void)setHeaderWidths{
int totalWidth = self.segSorter.bounds.size.width;
int areaWidth = 100,
priceWidth = 100;
int padding = 35;
[self.segSorter setWidth:totalWidth -padding- areaWidth-priceWidth forSegmentAtIndex:0];
[self.segSorter setWidth:areaWidth forSegmentAtIndex:1];
[self.segSorter setWidth:priceWidth forSegmentAtIndex:2];
}
Initially I thought it might be caused by the animation, but even adding a delay so the rotation animation has totally finished has no effect in the width. Are controls within a splitview given a false width or something?
One option you might consider is simply re-creating the control or removing then re-adding the segments.
Without knowing what's going on inside the control, as you change the widths, what if the first one you're resizing (say increasing width) would cause the sum of the widths to exceed the bounds? Without knowing how that logic is working inside, when I've run across things like this that seem like a fight for a simple change, it's easier to roll a method that just recreates the control as you need it (or removes and re-adds the elements) in the specified sizes.

UISlider minimumValueImageRectForBounds: and super maximumValueImageRectForBounds returns empty rectangle

I subclass UISlider in order to make the thumb smaller, and I want to override minimumValueImageRectForBounds and maximumValueImageRectForBounds to make their width 2px less. So my code is
- (CGRect)minimumValueImageRectForBounds:(CGRect)bounds
{
CGRect stdRect = [super minimumValueImageRectForBounds:bounds];
return CGRectMake(stdRect.origin.x + 2, stdRect.origin.y, stdRect.size.width - 2, stdRect.size.height);
}
The point is that stdRect is empty rectangle (0, 0, 0, 0).
Morover, if I explicitly set some rectangle like that
- (CGRect)minimumValueImageRectForBounds:(CGRect)bounds
{
return CGRectMake(2, 0, 40, 8);
}
It doesn't affect minimum value image position at all.
Any ideas?
Haha, I just figured it out. I wasn't reading closely enough.
Setting the minimumValueImage is not the same as calling setMinimumTrackImage:forState:. The minimumValueImage is an image that gets displayed to the left of the slider and is independent what's going on in the slider. Overriding minimumValueImageRectForBounds changes the dimensions of this image (the default size being 0pt wide) and the sliders frame is made less wide and shifted to the right as a result. As I understand it, there isn't a way to modify the rectangle of the minimumTrackImage (such as to make it extend to the right of the thumb image); it is only possible to change the image.
I haven't really figured out the point of allowing you to set the minimumValueImage. It seems like you could accomplish the same thing by changing the size of the slider and adding separate UIImageViews to the side of the slider. Who knows.
Note that everything I've said here applies in the same way for the maximumValueImage and setMaximumTrackImage:forState methods.
DO NOT DO THIS. This is an old, incorrect answer, by past me. I hate that guy.
--
As a general rule, you probably don't want to subclass the Apple standard UI controls. You might want to build it up and set those properties yourself instead (not sure if they are settable, but seems like they should be).

How to layout items "relatively" instead of absolute position?

What is the best way to layout iPhone UI items? Right now I am using CGRectMake and specifying the exact x and y coordinates of every UI item (label, view, button, etc) and adding every UI item to the current view.
One problem I see with this is that if one of the items change height or width, I will have to adjust the x and y coordinates of other UI items, so maintenance will be a challenge.
Is there an alternative to doing this? in HTML/CSS, I am used to just placing items relative to each other using margins, not absolute positioning. What do you recommend I do to keep maintenance easy and stress-free?
What I have been doing is using global constants to set the height, width, xoffset and yoffsets. Eg. I know I want 2 buttons next to each other, A and B where A is 20, 20 from origin and B is 20 right of A:
Constants.m:
CGFloat buttonHeight = 50, buttonWidth = 100;
CGFloat buttonAXoffset = 20, buttonAYoffset = 0;
CGFloat buttonBXoffset = 20, buttonBYoffset = 0;
CGFloat initialXoffset = 0, initialYoffset = 20;
(And use Constants.h to declare them with extern CGFloat blah)
To draw the buttons, set CGFloat creatorCursorX = initialXoffset, creatorCursorY = initialYoffset; which will be your relative cursor.
Then, just above creating the button A, do creatorCursorX += buttonAXoffset; creatorCursorY += buttonAYOffset;, and use creatorCursorX and creatorCursorY to set its position and buttonHeight and buttonWidth
After creating button A, do creatorCursorX += buttonWidth;
So when you get to making button B, you already have a cursor which you can use to place button B relative to button A just like above. Add offset before, add size after. And this makes it easy to switch buttons (and other layout items) around.
You can edit this method to place items down the screen as well.
Btw, to start the cursor on a new line, just set creatorCursorX = initialXoffset again and add the Y distance to creatorCursorY
I'm not an expert in this area, but I've found this method to be the most efficient and make it easy to adjust later. No need to sift through code, just shuffle chunks around for order, and go to the Constants.m to change the sizes.
I don't think there is an automatic way to achieve this.
I think your only option for this is to write a method, but even this is going to be tricky to work with, you'll need to determine the affected element and know which elements to reposition (see below for an extremely trivial example).
-(void)adjustUIElements:(CGSize)newSize{
//for example
button1.frame = CGRectMake(
(button1.frame.origin.x + newSize.width),
(button1.frame.origin.y + newSize.height),
button1.frame.size.width,
button1.frame.size.height);
//etc.
}
The great thing about this example is that you can run it any time there's a size change. It'll take a little while to get right but then you can reuse it in any function you see fit.
No. As for as I know, Relative Layout is not possible in iOS. You can use Interface builder to design the UI, which will be easier to position the UI elements.
You can also consider using the autoResizingMask property of UIView. Which is,
An integer bit mask that determines how the receiver resizes itself when its superview’s bounds change.
I couldn't find anything on the public-facing Apple site about it, but I read on one of the rumor sites that Apple will be introducing a new layout system in Lion called Cocoa Autolayout. Unfortunately, it won't be available for the next iOS release.
I don't want to link to any specific documents, but a search for "Cocoa Autolayout" on your favorite search engine will give you some general information how autolayout works.
If you're a registered iOS developer you should have access to one of the WWDC sessions on the topic.

StretchableImageWithLeftCapWidth stretching wrong portions

I am trying to use a UIImage with stretchableImageWithLeftCapWidth to set the image in my UIImageView but am encountering a strange scaling bug. Basically picture my image as an oval that is 31 pixels wide. The left and right 15 pixels are the caps and the middle single pixel is the scaled portion.
This works fine if I set the left cap to 15. However, if I set it to, say, 4. I would expect to get a 'center' portion that is a bit curved as it spans the center while the ends are a little pinched.
What I get is the left cap seemingly correct, followed by a long middle portion that is as if I scaled the single pixel at pixel 5, then a portion at the right of the image where it expands and closes over a width about twice the width of the original image. The resulting image is like a thermometer bulb.
Has anyone seen odd behavior like this and might know what's going on?
Your observation is correct, Joey. StretchableImageWithLeftCapWidth does NOT expand the whole center of the image as you would expect. It only expands the pixel column just right of the left cap and the pixel row just below the top cap!
Use UIView's contentStretch property instead, and your problem will be solved. Another advantage to this is that contentStretch can also shrink a graphic properly, whereas stretchableImageWithLeftCapWidth only works when making the graphic larger.
Not sure if I got you right, but LeftCapWidth etc is made for rounded corners, with everything in the rectangle within the rounding radius is stretched to fit the space between the 'caps' on the destination button or such.
So if your oval is taller or wider than 4 x 2 = 8, whatever is in the middle rectangle will be stretched. And yours is, so it would at least look at bit ugly! But if it's not even symmetrical, something has affected the stretch. Maybe something to do with origin or frame, or when it's set, or maybe it's set twice, or you have two different stretched images on top of each other giving the thermometer look.
I once created two identical buttons in the same place, using the same retained object - of course throwing away the previous button. Then I wondered why the heck the button didn't disappear when I set alpha to 0... But it did, it's just that there was a 'dead' identical button beneath it :)

Clipping within an UIView with some subviews

I have some buttons in an UIView. My problem is, that they get cut
off at the right side of the UIView. How do I prevent this?
alt text http://img.skitch.com/20090629-mj32p1bkff476256pwrpt69n2d.png
I've checked already Interface Builders clip property, but it's no
solution for this problem.
Regards
It seems like either you made these buttons programmatically, or you reiszed the initial IB view window to be larger and expected it to shrink down to the fit the screen.
The buttons in question cannot fit on the screen as they are - what effect are you looking for?
If you want the buttons all to fit you could set the text size to be smaller, and then they could fit.
If you want the buttons the size they are then you'll have to make another row, or put the buttons into a side scrolling container.
I have been using java and only recently began learning Apple's Obj-C framework.
An alternative to scrolling and row-breaking is using a "grid" layout with 1 row and n columns, where n is the number of buttons. Each cell has a fixed size. And you will have to resize your buttons (the subviews) in your superview's setNeedsLayout: method to whatever width you need such that all buttons fit the row.
See java's GridLayout class.
Kendall, thanks for your answer.
Here is my solution:
if(previousFrame.origin.x + theStringSize.width > 220){
roundedButton.frame = CGRectMake(15, previousFrame.origin.y + 30 , theStringSize.width + 8, theStringSize.height);
[myContainer insertSubview:roundedButton belowSubview:[tagsContainer.subviews lastObject]];
}else {
roundedButton.frame = CGRectMake(previousFrame.origin.x + previousFrame.size.width + 5, previousFrame.origin.y, theStringSize.width + 5, theStringSize.height);
[myContainer insertSubview:roundedButton belowSubview:[tagsContainer.subviews lastObject]];
}
I calculate, how many pixel I've moved from the left side. At some threshold (in my case 220) I start a new line.