Hello all,
I while back (about a month ago) I posted a question about how best to implement a looping menu. Here's the original post for reference, since I can't seem to find it on this board:
http://groups.google.com/group/cocos2d-iphone-discuss/browse_thread/thread/83843ac6a4de57c2/d03e1978c9d1ae03#d03e1978c9d1ae03
Anyways, I saw that Joao Caxaria had posted wanting to know how I implemented it. So, before I post the code, let me talk a little about how I did it, keeping in mind the following things:
1. I am still pretty brand new to Obj-C and cocos2d, though I am certainly not new to CS in general, so please, if I'm not doing something optimally, let me know.
2. This code isn't as generic as I'd like it to be. For example, it only supports horizontally scrolling menus right now, though it would be fairly trivial to add vertical support to it. In addition, items must be uniformly spaced (well, maybe, I haven't actually tried it). *Worst* of all, the menu only supports LoopMenuImageItems (though I'm pretty sure that I could make it more generic to just be a subclass of MenuItem, it's just I was working with ImageItems).
3. The code is still in its formative stages. *** Use this as a reference if you're just looking to see how I did it, but right now, selecting items, etc., is not supported, but I'm working on it and just wanted to share what I have (again, should be pretty trivial, just haven't gotten around to it as I'm busy).
As you can see, the code is pretty specific to my use case right now, but I think I will try and address these issues.
With that said, here's how I did it:
1. Created a Menu subclass called LoopingMenu (though ScrollingMenu might have been better). Also created LoopMenuImageItem (subclass of MenuImageItem with a flag and delta to indicate the need for proxy/proxy offset), and a ProxyableSprite (sprite subclasss with the draw() method overridden). The reason for the two classes instead of just a ProxyableSprite is that the images of a MenuImageItem are readonly, and rather than screwing with it (I assumed there was a good reason they were readonly), I sidestepped the issue.
2. Basically, I do some logic in ccTouchesMoved to calculate the left-most, left-most visible, right-most, and right-most visible items. "visibility" means an item is at least halfway inside the "menu width", which is defined as the sum of the widths of all its items + padding.
3. This determines whether items need a proxy, in which case they are flagged.
4. When an item is flagged as needing a proxy, its draw method, after drawing the primary copy of the image, will draw the same image at the specified offset (again, x-axis only right now).
Well, you're probably tired of reading this and just want the code:
#import <UIKit/UIKit.h>
#import "cocos2d.h";
typedef enum tagLMIS {
NO_INPUT = 1,
FIRST_TOUCH_DOWN,
SCROLLING, //these next two are mutually exclusive
FIRST_TOUCH_UP,
SECOND_TOUCH_DOWN
} LoopMenuIS;
@interface LoopMenu : Menu {
float menuWidth;
float menuHeight;
BOOL menuMoving;
LoopMenuIS inputState;
CFAbsoluteTime lastTouch;
}
@property (nonatomic) float menuWidth;
@property (nonatomic) float menuHeight;
@property (nonatomic) BOOL menuMoving;
@property (nonatomic) LoopMenuIS inputState;
@property (nonatomic) CFAbsoluteTime lastTouch;
- (NSArray *) getEdgeItems;
- (CGPoint)touchInMenu:(UITouch*) touch;
- (id)initWithItems:(MenuItem *) item vaList: (va_list) args;
- (BOOL)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (BOOL)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (BOOL)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
@end
@interface LoopMenuItemImage : MenuItemImage {
BOOL needsProxy;
float offset;
}
@property (nonatomic) BOOL needsProxy;
@property (nonatomic) float offset;
@end
@interface ProxyableSprite : Sprite {
LoopMenuItemImage *itemContainer;
}
@property (nonatomic, assign) LoopMenuItemImage *itemContainer;
@end
//Implementations
#import "LoopMenu.h"
@implementation LoopMenuItemImage
@synthesize needsProxy;
@synthesize offset;
- (id) init:(id) sender
{
if (!(self=[super init]))
return nil;
needsProxy = NO;
offset = 0;
return self;
}
-(id) initFromNormalImage: (NSString*) normalI selectedImage:(NSString*)selectedI disabledImage: (NSString*) disabledI target:(id) t selector:(SEL) sel
{
if( !(self=[super initWithTarget:t selector:sel]) )
return nil;
normalImage = [[ProxyableSprite spriteWithFile:normalI] retain];
[(ProxyableSprite *)normalImage setItemContainer:self];
selectedImage = [[ProxyableSprite spriteWithFile:selectedI] retain];
if(disabledI == nil)
disabledImage = nil;
else
disabledImage = [[ProxyableSprite spriteWithFile:disabledI] retain];
[normalImage setOpacity:opacity];
[selectedImage setOpacity:opacity];
[disabledImage setOpacity:opacity];
CGSize s = [normalImage contentSize];
transformAnchor = ccp( s.width/2, s.height/2 );
return self;
}
@end
@implementation ProxyableSprite
@synthesize itemContainer;
- (id) initWithFile:(NSString*) filename
{
if (!(self=[super initWithFile:filename]))
return nil;
[self setItemContainer:nil];
return self;
}
- (void) draw
{
glEnableClientState( GL_VERTEX_ARRAY);
glEnableClientState( GL_TEXTURE_COORD_ARRAY );
glEnable( GL_TEXTURE_2D);
glColor4ub( r, g, b, opacity);
[texture drawAtPoint: CGPointZero];
//Here's the changes
if ([itemContainer needsProxy])
{
[texture drawAtPoint:ccp(self.position.x + self.itemContainer.offset, 0)];
}
// is this chepear than saving/restoring color state ?
glColor4ub( 255, 255, 255, 255);
glDisable( GL_TEXTURE_2D);
glDisableClientState(GL_VERTEX_ARRAY );
glDisableClientState( GL_TEXTURE_COORD_ARRAY );
}
@end
@implementation LoopMenu
@synthesize menuWidth;
@synthesize menuHeight;
@synthesize menuMoving;
@synthesize inputState;
@synthesize lastTouch;
-(void) refreshMenuBox
{
int numItems = [self.children count];
if (numItems == 0)
{
self.menuWidth = 0;
self.menuHeight = 0;
return;
}
float itemWidth = [[self.children objectAtIndex:0] contentSize].width;
float itemHeight = [[self.children objectAtIndex:0] contentSize].height;
self.menuWidth = numItems * itemWidth + (numItems - 1) * 5; //5 is default padding (change later?)
self.menuHeight = itemHeight + 10; //little margin
}
- (NSArray *) getEdgeItems
{
float lep, lvep, rep, rvep;
lep = lvep = 1000.0;
rep = rvep = -1000.0;
MenuItemImage *lm = nil;
MenuItemImage *lmv = nil;
MenuItemImage *rm = nil;
MenuItemImage *rmv = nil;
for (MenuItemImage *item in children)
{
float convertedPosition = [self convertToWorldSpace:item.position].x;
if (convertedPosition <= lep)
{
lep = convertedPosition;
lm = item;
}
if (convertedPosition <= lvep && convertedPosition >= 0 - (item.contentSize.width / 2.0))
{
lvep = convertedPosition;
lmv = item;
}
if (convertedPosition >= rep)
{
rep = convertedPosition;
rm = item;
}
if (convertedPosition >= rvep && convertedPosition <= 320 + (item.contentSize.width / 2.0))
{
rvep = convertedPosition;
rmv = item;
}
}
return [NSArray arrayWithObjects: lm, lmv, rm, rmv, nil];
}
- (CGPoint)touchInMenu:(UITouch*) touch
{
//check to see if touch is 'in' our box
CGPoint location = [touch locationInView: [touch view]];
CGPoint convertedLocation = [[Director sharedDirector] convertCoordinate:location];
//gather heuristics about the menu
if (abs(convertedLocation.x - self.position.x) <= menuWidth / 2.0f &&
abs(convertedLocation.y - self.position.y) <= menuHeight / 2.0f)
return convertedLocation;
else
return ccp(-250, -250); //return bogus coordinates
}
- (id)initWithItems: (MenuItem *) item vaList: (va_list) args
{
if( !(self=[super initWithItems:item vaList:args]) )
return nil;
//replace the menu items with proxyable sprites
self.inputState = NO_INPUT;
self.lastTouch = CFAbsoluteTimeGetCurrent();
[self refreshMenuBox];
return self;
}
- (BOOL)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [self touchInMenu:touch];
if (location.x == -250 && location.y == -250)
{
//reset our input state and ignore
self.inputState = NO_INPUT;
return kEventIgnored; //touch not in menu space
}
//check to see if we are doing a double tap
if (self.inputState == FIRST_TOUCH_UP)
{
if (CFAbsoluteTimeGetCurrent() - self.lastTouch <= 1.0) //adjust this as necessary
self.inputState = SECOND_TOUCH_DOWN;
else
self.inputState = FIRST_TOUCH_DOWN; //too much time has elapsed
self.lastTouch = CFAbsoluteTimeGetCurrent();
}
else if (self.inputState == NO_INPUT)
{
self.inputState = FIRST_TOUCH_DOWN;
self.lastTouch = CFAbsoluteTimeGetCurrent();
}
else
return kEventIgnored; //ignore multi-touch
return kEventHandled; //handled!
}
- (BOOL)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [self touchInMenu:touch];
if (location.x == -250 && location.y == -250)
return kEventIgnored; //touch not in menu space (<--RESET FLAGS)
//get location of last touch
CGPoint old_loc = [touch previousLocationInView: [touch view]];
CGPoint old_location = [[Director sharedDirector] convertCoordinate:old_loc];
//calculate delta x
float delta = location.x - old_location.x;
if (delta >= 10)
self.inputState = SCROLLING;
//ok, move all items by delta x
for (LoopMenuItemImage *item in children)
{
[item setPosition:ccp(item.position.x + delta, item.position.y)];
//reset proxy info
item.needsProxy = NO;
}
//now check to see if we need to shift things
//grab the extrema
NSArray *extrema = [self getEdgeItems];
LoopMenuItemImage *lm, *lmv, *rm, *rmv;
lm = [extrema objectAtIndex:0];
lmv = [extrema objectAtIndex:1];
rm = [extrema objectAtIndex:2];
rmv = [extrema objectAtIndex:3];
if (lmv.position.x <= -(self.menuWidth / 2.0) + (lmv.contentSize.width / 2.0) + 1)
{
if (rm == rmv && lm == lmv) // proxy time
{
lmv.needsProxy = YES;
lmv.offset = rm.position.x - lmv.position.x + rm.contentSize.width + 5;
}
else if (rm == rmv && lm != lmv) //move leftmost item
[lm setPosition:ccp(rm.position.x + rm.contentSize.width + 5, lm.position.y)];
}
//NSLog(@"RMV: %f >= %f", rmv.position.x, (self.menuWidth / 2.0) - (rmv.contentSize.width / 2.0));
if (rmv.position.x >= (self.menuWidth / 2.0) - (rmv.contentSize.width / 2.0) - 1)
{
if (lm == lmv && rm == rmv) // proxy time
{
rmv.needsProxy = YES;
rmv.offset = lm.position.x - rmv.position.x - lm.contentSize.width - 5;
}
else if (lm == lmv && rm != rmv) //move rightmost item
[rm setPosition:ccp(lm.position.x - lm.contentSize.width - 5, rm.position.y)];
}
return kEventHandled;
}
- (BOOL)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSArray *extrema = [self getEdgeItems];
LoopMenuItemImage *lmv = [extrema objectAtIndex:1];
float delta = lmv.position.x - -(self.menuWidth / 2.0 - lmv.contentSize.width / 2.0);
NSLog(@"Delta %f: ", delta);
//first, move all items back to center by translating delta
for (LoopMenuItemImage *item in children)
{
item.needsProxy = NO;
[item runAction:[MoveTo actionWithDuration:0.5 position:ccp(item.position.x - delta, item.position.y)]];
}
if (self.inputState == NO_INPUT)
return kEventIgnored;
else if (self.inputState == FIRST_TOUCH_DOWN &&
CFAbsoluteTimeGetCurrent() - self.lastTouch <= 1.0)
self.inputState = FIRST_TOUCH_UP;
else if (self.inputState == SECOND_TOUCH_DOWN &&
CFAbsoluteTimeGetCurrent() - self.lastTouch <= 1.0)
//select item
self.inputState = NO_INPUT;
else if (self.inputState == SCROLLING)
self.inputState = NO_INPUT;
return kEventIgnored;
}
-(void) dealloc
{
[super dealloc];
}
@end
Again, please let me know if I could be doing something better (aside from the things I've already mentioned) - I'm kind of winging it as is, just looking at the API docs as I go along. I hope this helps anyone looking to implement a scrolling menu.
