Throu building a clock app, we’re exploring the uses of animation through Sprite kit for user interfaces instead of games. Last time we added the code to make a stopwatch. This time we will add three more buttons to control the stopwatch, and give them an animated presentation when the user presses the stopwatch button.
Add the Code for Making Buttons
Up to this point in the series we made sprites individually. Since we are making very similar sprites, a method for a button creation can be helpful:
-(SKSpriteNode *)makeButtonWithTitleAndName:(NSString *)title withImageNamed:(NSString *)imageName{ SKSpriteNode *aSprite = [SKSpriteNode spriteNodeWithImageNamed:imageName]; aSprite.name = title; [aSprite setScale:0.75]; SKLabelNode *titleNode = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"]; titleNode.text = title; titleNode.name = @"title"; //Will let us modify the title at runtime titleNode.fontSize = 16; titleNode.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter; titleNode.position = CGPointMake(CGRectGetMidX(aSprite.frame), CGRectGetMidY(aSprite.frame)); titleNode.fontColor = [UIColor blackColor]; [aSprite addChild:titleNode]; return aSprite; }
Line 2 and 3 of this code is nothing new — it creates a sprite. Line 4 changes the scale of the dot to three-quarters its original size. Line 5-12 is something we have yet to do. We set up a label node and then add it to the sprite. It now becomes a child of the sprite, and what we do to the sprite, we do to the label. Sometimes, we may need to change the label and not the sprite. In line 7, we assign the name title
to the label, and we can use the childNodewithName:
method on the sprite to change properties of the label when we need to make a change.
Positioning labels is a bit difficult due to the verticalAlignmentMode
property. By default, the vertical alignment mode is set to align to the baseline of the text. When centering a label on a sprite for a button, this will place the label too high on the button background. Line 9 changes the vertical alignment to SKLabelVerticalAlignmentModeCenter
which works a lot better for centering in the button.
Make a Method to Place the Buttons
Now that we have a method to make buttons, we can make some. Add the following code below what we just added:
-(void)makeStopWatchButtonsAtGridPoint:(CGPoint)gridPoint{ NSString *image = @"solidcircle"; //make button 1 -- start SKSpriteNode *startButton = [self makeButtonWithTitleAndName:@"Start" withImageNamed:image]; startButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y]; [self addChild:startButton]; //make button 2 -- stop SKSpriteNode *stopButton = [self makeButtonWithTitleAndName:@"Stop" withImageNamed:image]; stopButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y]; [self addChild:stopButton]; //make button 3 -- reset SKSpriteNode *resetButton = [self makeButtonWithTitleAndName:@"Reset" withImageNamed:image]; resetButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y]; [self addChild:resetButton]; //make a switch to a stopwatch SKSpriteNode *modeButton= [self makeButtonWithTitleAndName:@"Mode" withImageNamed:image]; modeButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y]; modeButton.scale = 1.0; [self addChild:modeButton]; }
Much of this code is a repetition. Had there been more buttons, a loop would have been a better way to code this. We get a button from our method above with different names, then place it at a specific grid point. Note that the last button we add is the Mode button, what was our original stopwatch on/off button. Sprite Kit adds sprites one on top of the other. The last node will be the visible top node. Since we want our mode button on top, we add that one last. We also want that to be full size, so the method changes the scale back to 1.0.
We can now add this to our code. Change the previous mode button code in initWithSize:
as follows:
//make a switch to a stopwatch //SKSpriteNode *modeButton= [SKSpriteNode spriteNodeWithImageNamed:@"solidcircle"]; //modeButton.name = @"modeButton"; //modeButton.position = [self gridPointX:4.0 pointY:2.0]; //[self addChild:modeButton]; //add the stopwatch buttons [self makeStopWatchButtonsAtGridPoint:CGPointMake(4.0, 2.0)];
Make an Animation Method for the Button
Our next step is to make the animation method for the buttons. Just like creating the buttons, the work is a bit repetitive. We first create a method to move a sprite horizontally on our layout grid. If the flag is yes we will show the sprite, if the flag is no, we will hide it.
(SKAction *)moveToGridPositionX:(CGFloat)gridPointX fadeIn:(BOOL)isFadein duration:(NSTimeInterval)duration{ CGPoint point = [self gridPointX:gridPointX pointY:0.0]; SKAction *move =[SKAction moveToX:point.x duration:duration]; SKAction *fadeInOut; if (isFadein){ fadeInOut = [SKAction fadeInWithDuration:duration ]; }else{ fadeInOut = [SKAction fadeOutWithDuration:duration]; } return [SKAction group:@[move,fadeInOut]]; }
Make a Method to Animate the Buttons
We now add our last method, which executes the animation. Again, we use a Boolean flag to either show or hide our button display. The buttons start hidden under the Mode button, but then spread out tot he left, while the mode button moves over to the side to be a semi-circle, and fade out its text.
-(void)animatedStopwatchMenuOpen:(BOOL)isOpen{ //a method to make buttons appear for the stopwatch NSTimeInterval duration = 1.0; SKAction *moveButton; //get our buttons SKNode *modeButton =[self childNodeWithName:@"Mode"]; SKNode *startButton = [self childNodeWithName:@"Start"]; SKNode *stopButton = [self childNodeWithName:@"Stop"]; SKNode *resetButton = [self childNodeWithName:@"Reset"]; SKNode *title; if (isOpen){ //push the on off to the side moveButton = [self moveToGridPositionX:5 fadeIn:YES duration:duration]; [modeButton runAction:moveButton]; title = [modeButton childNodeWithName:@"title"]; [title runAction:[SKAction fadeOutWithDuration:duration]]; //start to position 1 moveButton = [self moveToGridPositionX:1 fadeIn:YES duration:duration]; [startButton runAction:moveButton]; //stop to position 2 moveButton = [self moveToGridPositionX:2 fadeIn:YES duration:duration]; [stopButton runAction:moveButton]; //reset to position 3 moveButton = [self moveToGridPositionX:3 fadeIn:YES duration:duration]; [resetButton runAction:moveButton]; }else{ //close the menu moveButton = [self moveToGridPositionX:4 fadeIn:NO duration:duration]; title = [modeButton childNodeWithName:@"title"]; [title runAction:[SKAction fadeInWithDuration:duration]]; [startButton runAction:moveButton]; [stopButton runAction:moveButton]; [resetButton runAction:moveButton]; moveButton = [self moveToGridPositionX:4 fadeIn:YES duration:duration]; [modeButton runAction:moveButton]; } }
As discussed earlier, line 15 gives us access to the title of the button, and in this case we fade it out in line 16. We could have changed the title if we wanted to, but that requires a bit more work. The method childNodewithName:
returns a SKNode. We would have to cast, and possibly type check, the SKNode
to a SKLabelNode
in order to access the text
property.
The rest of this code moves sprite to the correct position. I could have easily used a loop here too, but for this example, I didn’t.
Hook it Up to the Clock App
In touchesBegan:
change the code to add the new animation method:
if (CGRectContainsPoint(modeButtonRect, [touch locationInNode:self])){ //code for stopwatch here //show the stopwatch buttons [self animatedStopwatchMenuOpen:YES];
and also here:
}else{ // shut down Stopwatch if on isStopWatch = NO; //move buttons back [self animatedStopwatchMenuOpen:NO];
The first set of code animates the buttons to open when the user presses the Mode button. When the user taps anywhere else, the buttons disappear.
In our next installment we will add some actions to our buttons, and start and stop the stopwatch.
The Whole Code
Here’s the code for the scene in one nice file.
// // MPMyScene.m // SpriteTimeClock // // Created by Steven Lipton on 5/6/14. // Copyright (c) 2014 Steven Lipton. All rights reserved. // #import "MPMyScene.h" @implementation MPMyScene{ SKLabelNode *myTimeLabel; SKLabelNode *myDateLabel; BOOL isShowingTime; BOOL isStopWatch; BOOL isStartingStopwatch; BOOL isRunningStopwatch; CFTimeInterval startTime; } -(CGPoint)gridPointX:(float)xPoint pointY:(float)yPoint{ CGFloat xDivision = CGRectGetMaxX(self.frame) /5.0; CGFloat yDivision = CGRectGetMaxY(self.frame)/5.0; return CGPointMake(xPoint * xDivision, yPoint * yDivision); } -(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { /* Setup your scene here */ myTimeLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"]; myTimeLabel.text = @"00:00:00"; myTimeLabel.fontSize = 40; myTimeLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMaxY(self.frame)*0.80); myTimeLabel.name = @"myTimeLabel"; [self addChild:myTimeLabel]; self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0]; myDateLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"]; myDateLabel.text = @"Date Goes Here"; myDateLabel.fontSize = 40; myDateLabel.alpha = 0.20; myDateLabel.position = [self gridPointX:2.5 pointY:3.0]; myDateLabel.name = @"myDateLabel"; [self addChild:myDateLabel]; //make a switch to a stopwatch //SKSpriteNode *modeButton= [SKSpriteNode spriteNodeWithImageNamed:@"solidcircle"]; //modeButton.name = @"modeButton"; //modeButton.position = [self gridPointX:4.0 pointY:2.0]; //[self addChild:modeButton]; //add the stopwatch buttons [self makeStopWatchButtonsAtGridPoint:CGPointMake(4.0, 2.0)]; //Make the ball CGPoint tick = [self gridPointX:0.5 pointY:1]; CGPoint tock = [self gridPointX:4.5 pointY:1]; SKSpriteNode *bouncingBall = [SKSpriteNode spriteNodeWithImageNamed:@"solidcircle"]; bouncingBall.position = tock; [bouncingBall setScale:0.75]; bouncingBall.name=@"bouncingBall"; [self addChild:bouncingBall]; /* //simple animation to the ball SKAction *bounceBall = [SKAction moveToX:tick.x duration:1.0]; [bouncingBall runAction:bounceBall withKey:@"bounceBall"]; */ //add animation to the ball SKAction *bounceBall = [SKAction sequence:@[ [SKAction moveToX:tock.x duration:1.0], [SKAction moveToX:tick.x duration:1.0], ]]; bounceBall.timingMode = SKActionTimingEaseInEaseOut; [bouncingBall runAction:[SKAction repeatActionForever:bounceBall] withKey:@"bounceBall"]; isShowingTime = YES; isStopWatch = NO; } return self; } -(SKSpriteNode *)makeButtonWithTitleAndName:(NSString *)title withImageNamed:(NSString *)imageName{ SKSpriteNode *aSprite = [SKSpriteNode spriteNodeWithImageNamed:imageName]; aSprite.name = title; [aSprite setScale:0.75]; SKLabelNode *titleNode = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"]; titleNode.text = title; titleNode.name = @"title"; titleNode.fontSize = 16; titleNode.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter; titleNode.position = CGPointMake(CGRectGetMidX(aSprite.frame), CGRectGetMidY(aSprite.frame)); titleNode.fontColor = [UIColor blackColor]; [aSprite addChild:titleNode]; return aSprite; } -(void)makeStopWatchButtonsAtGridPoint:(CGPoint)gridPoint{ NSString *image = @"solidcircle"; //make a base layeranchor is on the right side center. //anchor is right middle //make button 1 -- start SKSpriteNode *startButton = [self makeButtonWithTitleAndName:@"Start" withImageNamed:image]; startButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y]; [self addChild:startButton]; //make button 2 -- stop SKSpriteNode *stopButton = [self makeButtonWithTitleAndName:@"Stop" withImageNamed:image]; stopButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y]; [self addChild:stopButton]; //make button 3 -- reset SKSpriteNode *resetButton = [self makeButtonWithTitleAndName:@"Reset" withImageNamed:image]; resetButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y]; [self addChild:resetButton]; //make a switch to a stopwatch SKSpriteNode *modeButton= [self makeButtonWithTitleAndName:@"Mode" withImageNamed:image]; modeButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y]; modeButton.scale = 1.0; [self addChild:modeButton]; } -(SKAction *)moveToGridPositionX:(CGFloat)gridPointX fadeIn:(BOOL)isFadein duration:(NSTimeInterval)duration{ CGPoint point = [self gridPointX:gridPointX pointY:0.0]; SKAction *move =[SKAction moveToX:point.x duration:duration]; SKAction *fadeInOut; if (isFadein){ fadeInOut = [SKAction fadeInWithDuration:duration ]; }else{ fadeInOut = [SKAction fadeOutWithDuration:duration]; } return [SKAction group:@[move,fadeInOut]]; } -(void)animatedStopwatchMenuOpen:(BOOL)isOpen{ //a method to make buttons appear for the stopwatch NSTimeInterval duration = 1.0; SKAction *moveButton; //get our buttons SKNode *modeButton =[self childNodeWithName:@"Mode"]; SKNode *startButton = [self childNodeWithName:@"Start"]; SKNode *stopButton = [self childNodeWithName:@"Stop"]; SKNode *resetButton = [self childNodeWithName:@"Reset"]; SKNode *title; //open the menu -- do last //fade in the menu //scale the menu out to the edge if (isOpen){ //push the on off to the side moveButton = [self moveToGridPositionX:5 fadeIn:YES duration:duration]; [modeButton runAction:moveButton]; title = [modeButton childNodeWithName:@"title"]; [title runAction:[SKAction fadeOutWithDuration:duration]]; //start to poistion 1 moveButton = [self moveToGridPositionX:1 fadeIn:YES duration:duration]; [startButton runAction:moveButton]; //stop to position 2 moveButton = [self moveToGridPositionX:2 fadeIn:YES duration:duration]; [stopButton runAction:moveButton]; //reset to position 3 moveButton = [self moveToGridPositionX:3 fadeIn:YES duration:duration]; [resetButton runAction:moveButton]; }else{ //close the menu moveButton = [self moveToGridPositionX:4 fadeIn:NO duration:duration]; title = [modeButton childNodeWithName:@"title"]; [title runAction:[SKAction fadeInWithDuration:duration]]; [startButton runAction:moveButton]; [stopButton runAction:moveButton]; [resetButton runAction:moveButton]; moveButton = [self moveToGridPositionX:4 fadeIn:YES duration:duration]; [modeButton runAction:moveButton]; } } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { CGPoint topDisplay = [self gridPointX:2.5 pointY:4]; CGPoint bottomDisplay = [self gridPointX:2.5 pointY:3.0]; SKAction *labelHideAction = [SKAction group:@[ [SKAction fadeAlphaTo:0.2 duration:3.0], [SKAction moveToY:bottomDisplay.y duration:3.0] ]]; SKAction *labelShowAction = [SKAction group:@[ [SKAction fadeInWithDuration:3], [SKAction moveToY:topDisplay.y duration:3.0]]]; for (UITouch *touch in touches) { [myTimeLabel removeActionForKey:@"timeLabelAction"]; [myDateLabel removeActionForKey:@"dateLabelAction"]; SKNode *modeButton =[self childNodeWithName:@"Mode"]; CGRect modeButtonRect=modeButton.frame; if (CGRectContainsPoint(modeButtonRect, [touch locationInNode:self])&& !isStopWatch){ //code for stopwatch here //show the stopwatch buttons [self animatedStopwatchMenuOpen:YES]; //display the date on top if(isShowingTime){ [myTimeLabel runAction:labelHideAction withKey:@"timeLabelAction"]; [myDateLabel runAction:labelShowAction withKey:@"dateLabelAction"]; } isStopWatch = YES; isStartingStopwatch = YES; isShowingTime = NO; isRunningStopwatch = YES; }else{ // shut down Stopwatch if on isStopWatch = NO; //move buttons back [self animatedStopwatchMenuOpen:NO]; //move the button to the side //CGPoint point =[self gridPointX:4.0 pointY:2.0]; //[modeButton runAction:[SKAction moveToX:point.x duration:1.0 ]]; //toggle action if(isShowingTime){ [myTimeLabel runAction:labelHideAction withKey:@"timeLabelAction"]; [myDateLabel runAction:labelShowAction withKey:@"dateLabelAction"]; }else{ [myTimeLabel runAction:labelShowAction withKey:@"timeLabelAction"]; [myDateLabel runAction:labelHideAction withKey:@"dateLabelAction"]; } isShowingTime = !isShowingTime; } } } -(void)update:(CFTimeInterval)currentTime { // time display /* Called before each frame is rendered */ CFGregorianDate currentDate = CFAbsoluteTimeGetGregorianDate(CFAbsoluteTimeGetCurrent(), CFTimeZoneCopySystem()); CFRelease(CFTimeZoneCopySystem()); NSString *formattedTimeString = [NSString stringWithFormat:@"%02d:%02d:%02.0f", currentDate.hour, currentDate.minute, currentDate.second]; myTimeLabel.text = formattedTimeString; //date or stopwatch display if(isStopWatch){ //start the stopwatch by getting a point for elapsed time if(isStartingStopwatch){ startTime=currentTime; isStartingStopwatch=NO; } if (isRunningStopwatch) { //update the time in the stopwatch CFGregorianDate elapsedTime = CFAbsoluteTimeGetGregorianDate((currentTime - startTime), nil); NSString *formattedDateString = [NSString stringWithFormat:@"%02d:%02d:%02.1f", elapsedTime.hour, elapsedTime.minute, elapsedTime.second]; myDateLabel.text = formattedDateString; } }else{ NSString *formattedDateString =[NSString stringWithFormat:@"%04d %02d %02d", (int)currentDate.year, currentDate.month, currentDate.day]; myDateLabel.text = formattedDateString; } } @end
Leave a Reply