Thursday, January 29, 2009

Longer Spinning & Blurring

I can't tell you how many times I've been asked lately how to do the "UrbanSpoon Effect".

Note: There is a newer version of this code available here.


Now, I have no idea exactly how the folks at UrbanSpoon did their application, but they did it well. I don't have time for a full tutorial right now, or to do a full implementation of the effect, but in the process of helping somebody in a forum, I did write a little sample application that shows the basic theory of how to make your UIPickerView go round and round for longer.

As I said, this project nowhere near as polished as what UrbanSpoon does - I cheated on the blurring because I hit some bugs while porting my NSImage convolution kernel code to work with UIImage and I don't have the time to debug something that gnarly right now. My "blur" is just a double resize - down then back up. It's really just interpolation, but it sorta makes it blurry. I probably wouldn't use that method of blurring in a real application but, then again, I'd be getting paid for a real application.



This application does support using the accelerometer to start the spin. In order to make the picker view spin longer, I subclassed UIPickerView. My subclass had exactly one method. This is an undocumented method that returns the duration of the spin, so you just return the number of seconds you want your spin to go for:

#import "JLPickerView.h"

@implementation JLPickerView
- (double)scrollAnimationDuration
{
return 5.0;
}
@end


The other key thing to note in this application is that my UIPickerViewDataSource lies about how many rows each component has. I have a constant called kRowMultiplier. I take the count of the array that corresponds to a particular component, and I multiply it by kRowMultiplier. This value is currently set to 100, so if the array that feeds a component has ten items, my datasource lies and says there are a thousand, and then it just keeps feeding the same ten items over and over in the same order by calculating the actual row in the array to use:

int actualRow = row%[[self arrayForComponent:component] count];

This creates a component with the same values repeated over and over.

When the view first appears, I load arrays with views containing both the blurred and unblurred data. You don't want to be programmatically blurring the values over and over when the spin happens, so we do it once and store the values in arrays. I then set the component's row to a value in the middle. This means that the user will never see the blank rows above and below the real values, which helps create the illusion of endless spinning.
[picker selectRow: [component1Data count] * (kRowMultiplier / 2) inComponent:0 animated:NO];

Now, when a value is selected in the picker, I do a bait-and-switch back to the middle. I move the component to the same value they're currently on, but back in the middle of the wheel. This way, no matter what happens, they'll never see the blank rows at the top or bottom. Since I'm moving them to the same value and the surrounding values are the same, and I tell it not to animate the change, the user has no idea it has happened.
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
NSArray *componentArray = [self arrayForComponent:component];
int actualRow = row%[componentArray count];

int newRow = ([componentArray count] * (kRowMultiplier / 2)) + actualRow;
[picker selectRow:newRow inComponent:component animated:NO];

}

Finally, when the spin button is pressed, or the user shakes the phone, I calculate a random number for each component. This number is a value between 0 and the size of the array minus 1. This tells me what the new, randomly selected value will be. Then I move the components (again, without animating) to the corresponding row near the top or bottom of the dial, and then animate to the selected value at the other end of the wheel.
- (IBAction)spin
{
if (! isSpinning)
{
// Calculate a random index in the array
spin1 = arc4random()%[component1Data count];
spin2 = arc4random()%[component1Data count];
spin3 = arc4random()%[component1Data count];

// Put first and third component near top, second near bottom
[picker selectRow:([picker selectedRowInComponent:0]%[component1Data count]) + [component1Data count] inComponent:0 animated:NO];
[picker selectRow:(kRowMultiplier - 2) * [component2Data count] + spin2 inComponent:1 animated:NO];
[picker selectRow:([picker selectedRowInComponent:2]%[component3Data count]) + [component3Data count] inComponent:2 animated:NO];

// Spin to the selected value
[picker selectRow:(kRowMultiplier - 2) * [component1Data count] + spin1 inComponent:0 animated:YES];
[picker selectRow:spin2 + [component2Data count] inComponent:1 animated:YES];
[picker selectRow:(kRowMultiplier - 2) * [component3Data count] + spin3 inComponent:2 animated:YES];

isSpinning = YES;

// Need to have it stop blurring a fraction of a second before it stops spinning so that the final appearance is not blurred.
[self performSelector:@selector(stopBlurring) withObject:nil afterDelay:4.7];
}
}


I set a variable called isSpinning so that I know whether to provide blurred or unblurred data to the spinner. I have the spin set for 5 seconds, so 4.7 seconds in the future, I set the value back to NO. This cause it to stop blurring a moment before the spinning stops. This is done so that the final words displayed will not be blurred.

You can download the sample project here.

One note, though: I have used a couple of undocumented, private methods in this project. I ordinarily shy away from doing that, but in the sake of getting this code out the door quickly, I did. I use one undocumented method for resizing the UIImage in order to create the fake blur effect, and another one to keep the picker from making sounds when I initially set the values because I don't want the user to realize I'm moving the picker - that would shatter the illusion. These are pretty subtle uses of undocumented methods so I doubt they'd get your application rejected, but they could. They're also not methods that are likely to change. But, again, they could - that's always a risk with undocumented methods, so caveat emptor - use at your own risk. If you want to play it safe, to a real motion blur and take out the calls to UIPickerView's setSoundsEnabled: method.



17 comments:

Kyle Hayes said...

I know you did this code in a hurry but one thing I wanted to note about it to other developers was that if you click one of the values that below the highlight bar, it takes the full 5 seconds to animate that one position move.

iScunner said...

Jeff,

With regards to Urbanspoon, it appears their solution is nowhere near as complex as yours. They're overlaying the picker wheels with animation frames, giving the illusion of fast spinning. If you watch the animation carefully, you'll see that the colour or shade of the picker wheels changes slightly when the wheels come to rest. Another thing I noticed is that the location wheel looks the same regardless of whether you choose Cincinnati or Omaha!

Gordon

Jeff LaMarche said...

Kyle:

You're right - it works fine if you "fling" the wheel, but not if you move it slowly to a new position. Not sure how to fix that, probably will involve diving into UIPickerView in a way that Apple wouldn't condone...

Gordon:

Thanks for the info. Living in a rural area with few restaurants, I don't actually have UrbanSpoon on my phone, I was just using the YouTube video as a guide. Looks like I made things harder than necessary...

Kyle Hayes said...

I saw someone mention on another forum post that they inspected the app bundle for urbanspoon and found a series of 15 or so PNGs that were pre-rendered "steps" of the pickers. Tedious but probably less overhead.

iScunner said...

I'm impressed that you figured out how to achieve a similar effect with code, where Urbanspoon has to reserve ~4MB of memory for its images. Not a lot, I know, but you've got to appreciate the elegance of the code.

iPhone SDK Articles said...

Great post Jeff. That is a cool idea to animate the picker view. I would have never thought about doing it this way.

Happy Programming,
iPhone SDK Articles

Jeff LaMarche said...

Thanks for the kind words, guys. Unfortunately, I think the issue that Kyle pointed out might make this code no good for production use unless your app didn't need to let the user manually spin the wheels.

Of course, we could have two identical pickers, unhide the "long spinning picker" when they shake or press the button, and hide it again when the spinning stops.

Would be a bit of pain keeping the two in sync, but would probably work - as long as they were the same and you didn't animate it, the user shouldn't be able to tell that there are actually two spinners.

I might try this if I get some time.

Jeff LaMarche said...

Check the latest post for an updated version...

digital_eyezed said...

Well done on this, I have to say I wouldn't have gone down the coding route, I would have diverted to photoshop to create some images and then animate them. One question though, I have tried to run this code and sometimes (almost every time) some of the labels disapear when it stops or when you move the picker manually, do you know what this might be? I am thinking of using this in an app I'm working on but would need to fix that issue first.

Cheers and well done again!

Iain

pooja said...

Hello
i seen your longer spin code of pickerviw that give spin functionily of urbanspoon application .i also add your code in my application .as you in code you used a kmutipier and value of it is 100 .according to your code pickes moves in both direction (up and botton .but i need its move only bottom .i also did it but speed and smoothness are not like urbanspoon .
so plz give suggestion how to do this.
as soon as give the reply.
bye&tc
Pooja Rusia

lachmish said...

Hey Jeff thanks for a great post are there any news on the Q of digital_eyezed ... having the same problem love to hear what do u have to say about it thx

lachmish said...

Is there a way to control the UIPickerView animation speed?

Marichka said...

to lachmish:

in order to control animation speed you should do the following:

@interface UIPickerView(MyExt)
- (double)scrollAnimationDuration;
@end

@implementation UIPickerView(MyExt)

- (double)scrollAnimationDuration
{
return SpinningTime;//Put any number you want here
}

@end

iDeveloper said...

Just an FYI. This code will be rejected if you submit it now. Ever since Apple used the static analyzer you cannot use private methods, in this case _imageScaledToSize:interpolationQuality:

Manaconda said...

Jeff, I am not using the blurring effect, but I am using the kRowMultiplier technique to make the picker loop.

This works great for me except for one thing. I have a matching game where you match an image in component one to the same image in component two. When you do, I remove that element from the datasource and reload the uipicker.

Everything works until I get down to two or especially one image left (on kRowMultiplier separate rows). When this happens, whenever I spin the wheel, the image disappears when it gets near the center.

I think this is the same issue that digital_eyezed and lachmish are having.

Does anyone know of a way to solve this issue?
I did a check and set kRowMultiplier back to 1 when I have 2 items left in the datasource, and this works, but I would really really like a better solution.

Edwin said...

scrub m65 kamagra attorney lawyer body scrub field jacket lovegra marijuana attorney injury lawyer

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels