Thursday, March 19, 2009

Gradient Shading UIView

Today I want to do something similar to the UIRoundedView, except this time I want the background to be coloured using a gradient shading. Something like this:



So, like last time, we will extend the UIView class. We are going to require an extra class property, which is going to be the one of 2 colours we use to specify our gradient. We will use the backgroundColor property as the other one. We are only concerned with a linear vertical gradient... of course it would be easy to extend this class to handle other types of gradient, but this is all I need for now. We are going to use CGContextDrawLinearGradient to create the effect, which takes a CGGradientRef, so lets include that in our class definition as well:


#import <UIKit/UIKit.h>


@interface UIGradientView : UIView {
CGGradientRef gradient;
UIColor *gradientColor;
}

@property (nonatomic, retain) UIColor* gradientColor;
@end


Next up, we want to create the CGGradientRef based on gradientColor and backgroundColor. One thing that was a little tricky here is that the colour spaces of each colour passed to CGGradientCreateWithColorComponents must be the same. However, [UIColor *Color] doesn't always return the same colour space. So we need some kind of conversion, which we need to perform for both backgroundColor and gradientColor.

CGGradientCreateWithColorComponenets takes a colour space, an array of colour components, an array of locations, and a count.

First up, we need to figure out how many components the colour has:


CGColorRef col = self.backgroundColor.CGColor;
CGColorSpaceRef colSpace = CGColorGetColorSpace(col);
NSLog(@"Color Space contains: %d", CGColorSpaceGetNumberOfComponents(colSpace));

We extract the components using CGColorGetComponents which will return a float array with n + 1 entries, where n is the value returned above.


const CGFloat *bgCols = CGColorGetComponents(col);
if(CGColorSpaceGetNumberOfComponents(colSpace) == 1) // White Color Space
{
colors[0] = bgCols[0];
colors[1] = bgCols[0];
colors[2] = bgCols[0];
colors[3] = bgCols[1];
}
else
{
colors[0] = bgCols[0];
colors[1] = bgCols[1];
colors[2] = bgCols[2];
colors[3] = bgCols[3];
}

The extra value returned is the alpha value. Conversion from a gray scale colour scheme to rgb, is simple, we just assign the same value to R G and B entries. We need to repeat this process for the gradient colour, and then we create our CGGradientRef:


gradient = CGGradientCreateWithColorComponents(rgb, colors, NULL, sizeof(colors)/(sizeof(colors[0])*4));

Now that we have created the gradient, we can do the actual drawing. Same as last time, we will be overriding drawRect:, but the code is pretty trivial this time:


- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();

CGPoint start = CGPointMake(rect.origin.x, rect.origin.y);
CGPoint end = CGPointMake(rect.origin.x, rect.origin.y + rect.size.height);

CGContextClipToRect(context, rect);

CGContextDrawLinearGradient(context, gradient, start, end, 0);

And thats about it as far as interesting code is concerned. We should create the gradient as few times as possible, so when either the backgroundColor or gradientColor change, and we need to define some behaviour if only one is set (ie if backgroundColor is set, but gradientColor is not, just fill the background as per usual).


Addendum

[UIColor whiteColor] [UIColor blackColor] [UIColor grayColor] all return a colour in a colour space with a single component, where as [UIColor redColor] returns an RGB colour space.

Wednesday, March 18, 2009

iPhone OS 3.0

As if there is anything worth blogging about today apart from iPhone OS 3.0!

I have my pre-release version of both the SDK and the OS downloaded already. The SDK installs cleanly, as you would expect, and really there are not any huge visible differences... but dig a little deeper and there is some cool stuff here. First things first... I fired up Word Salad to ensure it still works as expected (it does, at least in the simulator).

Installed iPhone OS 3.0 on my test phone, but to complete the install it requires iTunes 8.1 (I had 8.0)... updating...

Everything seems fine here! Yay!

Wednesday, March 11, 2009

Pausing during calls

I realised yesterday (actually my wife did) that receiving a call during game play of WordSalad doesn't work how it should. The timer keeps ticking away but you are unable to provide any input.

Input is withheld from the UIView's when an event happens (ie receiving an SMS, a phone call or a calendar reminder), but the app does keep running. So it turns out its pretty simple what needs to be done. The application delegate needs to respond to three events:


-(void)applicationWillResignActive:(UIApplication *)application {
[[self rootViewController] pauseGame];
}

-(void)applicationWillTerminate:(UIApplication *)application {
[[self rootViewController] saveSettings];
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
[[self rootViewController] unpauseGame];
}

The sequence of events is easy to understand.
  1. Application starts
  2. Interruption occurs (phone/sms/calendar), applicationWillResignActive is called
  3. 2 options here. Either the user:
  • a) Rejects the call/sms/calendar event, and applicationDidBecomeActive is called
  • b) Accepts the call/sms/calendar event, and applicationWillTerminate is called
Now, as it happens, I already had the applicationWillTerminate: code in place, as this is where your application should save its settings to allow for continuing where the user left off. So really I just needed some code to pause and unpause the game. This was very simple. Rather than mess around killing timers and then restarting them, I simply made the timer return early if the game was 'paused'. Since input is not getting passed to the app, as long as nothing moves (including the timer) the game appears paused.

Unpausing was simply a matter of setting gamePaused = NO. Too easy!

Of course, WordSalad is a simple game, so there is almost nothing happening in the background, apart from the timer, it is purely event driven. Game loops probably shouldn't run full speed during an interruption... there is no point wasting that power if the user cannot actually interact.


Monday, March 9, 2009

Osaka

Not much going on today, just recovering from a weekend in Osaka. Very cool place, and I did get to see Sony's OLED TV (all 11 inches of it), and the new Panasonic plasma's which we wont see until much later in the year. Very cool!

Thursday, March 5, 2009

Muffin #2

The muffin support people got back to me and said they are working on the issues I raised. Was nice to get some thanks for the feedback I sent in! :) Hopefully they have a new version for me to poke soon!

Wednesday, March 4, 2009

UIRoundedView

Today I wanted to add a little view to my iPhone app, to display some text (an about box). However, I wanted it to look a little more interesting than just a plain square view... perhaps a box with nicely rounded corners!


This is pretty easy to do using the iPhones SDK. The first thing we need to do is subclass UIView so we can make our rounded UIView, so lets create a header which looks like this:



#import <UIKit/UIKit.h>


@interface UIRoundedView : UIView {
}
@end

Easy! But not very useful yet. Lets have a look at the code side of things. The important member of UIView we need is drawRect:


- (void)drawRect:(CGRect)rect {
int radius = 10;

CGContextRef context = UIGraphicsGetCurrentContext();
[self drawRounded:rect With:radius On:context];
CGContextFillPath(context);
}

The drawRect function will get called anytime this view is updated. The drawRounded function contains the actual code, but lets break it down piece by piece. Firstly, we start the path. This is tells the drawing code, be it drawing a line, or filling a shape, where the bounds are.

// Get the current drawing context
CGContextRef context = UIGraphicsGetCurrentContext();

CGContextBeginPath(context);

Next up, we set a starting point


CGContextMoveToPoint(context, x1, y1);

And then for the corner:


CGContextAddArcToPoint(context, cx, cy, x2, y2, radius);

It needs to be noted that (cx,cy) and (x1,x2) are not the same point. In fact, (cx,cy) is not a point on the line at all. The current position (which we set with CGContextMoveToPoint() and is currently (x1,x2) ) and (x2,y2) will form the end points of the arc.

Next is a simple matter of drawing a line from the 'current' position, which is updated by CGContextAddArcToPoint(), to
the start of the next corner curve:


CGContextAddLineToPoint(context, x1, y1);

Now, lets look at the whole function. Rather than type this out 4 times, I have placed it in a loop.


-(void)draw: (CGRect)rect With:(float) radius On:(CGContextRef) context {
float x1,x2,y1,y2; // x1,y1 is the starting co-ord, x2,y2 is the end point
float cx, cy; // The tangent intersect point
corner_t corner;

CGContextBeginPath(context);

for(int i = 0; i < 4; i++) {
corner = i;

switch (corner) {
case topleft:
x1 = rect.origin.x;
y1 = rect.origin.y + radius;

cx = rect.origin.x;
cy = rect.origin.y;

x2 = rect.origin.x + radius;
y2 = rect.origin.y;
break;
case topright:
x1 = rect.origin.x + rect.size.width - radius;
y1 = rect.origin.y;

cx = rect.origin.x + rect.size.width;
cy = rect.origin.y;

x2 = rect.origin.x + rect.size.width;
y2 = rect.origin.y + radius;
break;
case bottomright:
x1 = rect.origin.x + rect.size.width;
y1 = rect.origin.y + rect.size.height - radius;

cx = rect.origin.x + rect.size.width;
cy = rect.origin.y + rect.size.height;

x2 = rect.origin.x + rect.size.width - radius;
y2 = rect.origin.y + rect.size.height;
break;
case bottomleft:
x1 = rect.origin.x + radius;
y1 = rect.origin.y + rect.size.height;

cx = rect.origin.x;
cy = rect.origin.y + rect.size.height;

x2 = rect.origin.x;
y2 = rect.origin.y + rect.size.height - radius;
break;
default:
break;
}

// Start the path if its the first iteration
if(i == 0)
{
CGContextMoveToPoint(context, x1, y1);
}
else
{
CGContextAddLineToPoint(context, x1, y1);
}

// Draw the corner arc
CGContextAddArcToPoint(context, cx, cy, x2, y2, radius);
}

CGContextClosePath(context);
}

So now we have a UIView that fills itself... however we haven't specified a colour, and worse, if we set a background colour in InterfaceBuilder, we are going to see that and its going to have square edges! So we need to do a bit more work. We need to override backgroundColor. We need to do this, because we want the base UIView's backgroundColor to always be [UIColor clearColor].

So lets override setBackgroundColor and backgroundColor:


-(void)setBackgroundColor: (UIColor *) color {
self.fillColor = color;
[self setNeedsDisplay];
}

-(UIColor *)backgroundColor {
return self.fillColor;
}

Note that I have called [self setNeedsDisplay], this will ensure that if at any point you assign a new background colour
to the view, it will get updated. The other thing I want to add is a border. The easiest way to do this, is simply call our drawing
code twice:


- (void)drawRect:(CGRect)rect {
int radius = 30;

CGContextRef context = UIGraphicsGetCurrentContext();

[fillColor setFill];
[self draw:rect With:radius On:context];
CGContextFillPath(context);

CGContextSetLineWidth(context, borderWidth);
[borderColor setStroke];
[self draw:rect With:radius On:context];
CGContextStrokePath(context);

}

Now a member for borderColor as well, and we have a useful UIRoundedView class!


One small gotcha to be aware of, is that setting defaults in initWithFrame() isn't going to work if you are using IB to create your objects. Use initWithCoder instead.

And here is the result!


Source available: UIRoundedView.m and UIRoundedView.h

Tuesday, March 3, 2009

Figuring out what your system is doing

If you use windows, then you probably wonder what its doing at times... 

2 very neat tools for doing this are procmon and procmgr  (both from technet). procmgr is like task manager on steriods, and procmon allows you to see what file, registry and net access a program is using.

So when songbird hangs (like it just did! back to foobar), you can poke around and see what its doing, and wonder why it has 4 sockets open, and 20+ threads and using over 100 megs of ram!

Mark Russinovich has a nice example of using both tools.

First Post!

Original huh? Have to start some way I guess! 

I have spent some time lately exploring different music players. This started due to this article at ars about 'muffin'. Muffin uses some music ID algorithm and based on several factors recommends similar tracks. Sounded like pretty neat technology, so figured I would give it a shot.

I have a collection of roughly 9000 FLAC files, all ripped from my CD collection, so I figured that would be a good starting point for it. The collection is pretty mixed, although it has a fair amount of dance music, and also a fair amount of metal... and a bunch of stuff in between. The music recommendation was hugely hit and miss. For more poppy mainstream tunes, it seemed to work pretty well, for songs that are pretty consistent in style all the way through, it worked okay. However, for DJ mixes it was terrible. For anything experimental, or not easily defined, it was hilariously bad. Sure, figuring out what is similar to Stolen Babies or Unexpect or even Mr Bungle is not going to be easy.

Anyway, this got me to thinking that its a really neat idea (however badly implemented currently), and has promise. But... muffin player is buggy. I sent them a nice long email with a bunch of bugs.... and this got me looking for other music players. However, each and every one I tried was lagging features somewhere!

Here is a quick list:
  • Muffin: No album cover art, playback stutters, laggy
  • Songbird: Its big and slow... and uses a tonne of memory. Multi-media keys on my keyboard do not work with it. No album random, no cddb, meta data editing is primitive
  • QMP: No gapless playback!
  • Foobar2000... what I use, and have used for a long time: No album random. Thats about it really.
There are other players such as WinAmp that I have tried previously and ditched for various reasons that I don't recall now. My list of requirements doesn't seem extreme (to me!):
  1. Good quality playback. This is very important, stuttering because I read a file on the same disk as the audio should never happen. Ideally ASIO or Kernel Streaming support. 
  2. Fast response. If I click next track, do it immediately, not in 2 seconds. Ditto with skipping to a section of a song.
  3. Low memory footprint. Really, all you should be doing is playing music and presenting some information about it. Using flash (I'm looking at you muffin) is not helping here. Using something like SQLite as a back end is a great idea. Small and fast. (QMP does this).
  4. Use my multimedia keyboard... the audio buttons are the only thing I find useful on it, especially if I am playing a game or something.
  5. Show me cover art. I went through the pain of finding and downloading it.
  6. Album Random!
Ok so 5 and 6 are not that important, but are nice to have. Foobar actually fits 5/6 of the above, which is why I keep using it. But I would like album random (random selection per album, not per track.... random is really crappy with live mixes).

Note that plugins are not important to me, assuming that the main player does what I need. Oh, I nearly forgot:

   7.  FLAC support!

In case its not obvious, I am talking about windows here. I just use iTunes on my mac -- it has its deficiencies but works nicely with my ipod/iPhone.... and I tend not to listen to music there anyway.... and I haven't booted into Linux for a very long time.