expandable_menu

The Path 2.0 iPhone app (http://path.com) uses an expandable menu to free up some extra real estate. The Path app places a button in the lower left corner of the screen. When the user presses the button, menu items expand out from behind the button in a circular pattern at a fixed distance from the main button. To close the menu the user either selects one of the options presented or presses the main button again.

image

The following is a quick overview of how I implemented a navigation system similar to the Path iPhone App.

Source Code

The full source code for the project can be found on github.com at https://github.com/tobins/PathMenuExample. Feel free to use the code provided in the example as you wish. Just say hi on twitter (@tobins) if you found this post useful.

Setup

To keep things simple I set up a single class named ExpandableNavigation that will handle all the work. It has an init method that takes in an array of menu item buttons (UIViews), a main button, and distance the menu item buttons should travel from the center of the main button when expanded. I did this because I wanted developers to set up and configure buttons either programmatically or using interface builder.

For example here is the code for ViewController.m’s viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];

    // initialize ExpandableNavigation object with an array of buttons.
    NSArray* buttons = [NSArray arrayWithObjects:button1, button2, button3, button4, button5, nil];

    self.navigation = [[[ExpandableNavigation alloc] initWithMenuItems:buttons
                                                            mainButton:self.main
                                                                radius:120.0] autorelease];
}

The menu item buttons (1-5) were all added to the ViewController.xib and wired to UIButtons defined in ViewController.h. The main button is wired to the “main” UIButton. Passing in the array & the main button to the init method sets up the initial positioning of the menu item buttons and attaches a touch event to the main UIButton for handling the menu expanding/collapsing.

IMPORTANT NOTE: Make sure the main menu button is in front of all of the menu items you want controlled by the class.

As you can see below, the init method is pretty basic.  It sets up some variables, aligns the menu items buttons to the center of the main button, and attaches a touch event to the main button.

- (id)initWithMenuItems:(NSArray*) menuItems mainButton:(UIButton*) mainButton radius:(CGFloat) radius {

    if( self = [super init] ) {
        self.menuItems = menuItems;
        self.mainButton = mainButton;
        self.radius = radius;
        self.speed = 0.15;
        self.bounce = 0.225;
        self.bounceSpeed = 0.1;
        expanded = NO;
        transition = NO;

        if( self.mainButton != nil ) {
            for (UIView* view in self.menuItems) {

                view.center = self.mainButton.center;
            }

            [self.mainButton addTarget:self action:@selector(press:) forControlEvents:UIControlEventTouchUpInside];
        }
    }

    return self;
}

Expanding

When the main button is pressed the menu item buttons associated to it will expand from the center of the main button to the distance given in the init method. The menu items will be animated to bounce out and be equally spaced along the 90 degree edge of a circle.

The expand method uses block-based UIView animations and CGAffineTransformMakeRotation to handle the expanding of the menu.

- (void) expand {
    transition = YES;

    [UIView animateWithDuration:self.speed animations:^{
        self.mainButton.transform = CGAffineTransformMakeRotation( 45.0 * M_PI/180 );
    }];

    for (UIView* view in self.menuItems) {
        int index = [self.menuItems indexOfObject:view];
        CGFloat oneOverCount = self.menuItems.count<=1?1.0:(1.0/(self.menuItems.count-1));
        CGFloat indexOverCount = index * oneOverCount;
        CGFloat rad =(1.0 - indexOverCount) * 90.0 * M_PI/180;
        CGAffineTransform rotation = CGAffineTransformMakeRotation( rad );
        CGFloat x = (self.radius + self.bounce * self.radius) * rotation.a;
        CGFloat y = (self.radius + self.bounce * self.radius) * rotation.c;
        CGPoint center = CGPointMake( view.center.x + x , view.center.y + y);
        [UIView animateWithDuration: self.speed
                              delay: self.speed * indexOverCount
                            options: UIViewAnimationOptionCurveEaseIn
                         animations:^{
                             view.center = center;
                         } 
                         completion:^(BOOL finished){
                             [UIView animateWithDuration:self.bounceSpeed
                                              animations:^{
                                                  CGFloat x = self.bounce * self.radius * rotation.a;
                                                  CGFloat y = self.bounce * self.radius * rotation.c;
                                                  CGPoint center = CGPointMake( view.center.x - x , view.center.y - y);
                                                  view.center = center;
                                              }];
                             if( view == self.menuItems.lastObject ) {
                                 expanded = YES;
                                 transition = NO;
                             }
                         }];                                                                        
    }
}

Note that the end point for the initial animation is farther than the radius specified in the init method. I did this so I could set up a second animation to do the “bounce” to the desired distance. You’ll see that in the completion block that I set up a second animation that returns the UIView back to the desired distance.

Collapse

The collapse method returns the menu item buttons back behind the main menu button.

- (void) collapse {
    transition = YES;

    [UIView animateWithDuration:self.speed animations:^{
        self.mainButton.transform = CGAffineTransformMakeRotation( 0 );
    }];

    for (UIView* view in self.menuItems) {
        int index = [self.menuItems indexOfObject:view];
        CGFloat oneOverCount = self.menuItems.count<=1?1.0:(1.0/(self.menuItems.count-1));
        CGFloat indexOverCount = index * oneOverCount;
        [UIView animateWithDuration:self.speed
                              delay:(1.0 - indexOverCount) * self.speed
                            options: UIViewAnimationOptionCurveEaseIn
                         animations:^{
                             view.center = self.mainButton.center;
                         } 
                         completion:^(BOOL finished){                            
                             if( view == self.menuItems.lastObject ) {
                                 expanded = NO;
                                 transition = NO;
                             }
                         }];                                                                         
    }
}

The collapse method is a little simpler than the expand as it doesn’t do a bounce when returning the menu items to behind the main button.

Extra Credit

I didn’t implement the Path 2.0 exactly as it is in the app, however this gives you a good starting point. Incase you’re interested, fork the repository and try your hand at one of the following:

* Calculate the ideal radius based on number of menu item buttons (and their sizes) passed to the init method and size of those buttons.

* Spin the menu items like the Path 2.0 App using UIView or Core Animation.

* Auto adjust the z-index of the views in the init method so that the main menu button is always on top.

Enjoy and don’t forget to say hi on twitter!

@tobins

  • Carlo Di Domenico

    hi Tob.in, your button is just awesome! I have a question: while playing around with the “ExpandableNavigation” class I tried to import it in another project but strangely the mainButton translates a bit while rotating when pressed. Am I doing something wrong????

    • http://tob.in tobin

      Hi! You most likely did not do something wrong…

      I haven’t updated that code in a few years. It was originally written for iOS 5 and I’m not sure if it would work without changes on iOS 7. I will try to take a look at the code when I have some free time and update it to iOS 7.

      • Carlo Di Domenico

        Ok, thank you very much!
        For what I can say, by deleting the mainButton animation in the “- (void) expand ” all the rest seems to work fine.

        bye

  • Alessio

    Hi Tob.in and thank you for your work. Your button is really cool and i’m experiencing same carlo’s problem… if i try your example project directly on my iphone (iOS 6.1), it works perfectly… But if i include your classes and resources in my app, again with ios 6.1, it behaves like carlo said… the main button rotates and moves, and the other buttons move in an incorrect way… If i remove the animation for the main button in the expand method, everything is fine… what’s wrong?
    Thanks and many compliments.