The Exploding Button

I’ve been working on an app with my brother, and we’ve wanted the experience to really stand out from the crowd. He’s tracking statistics that are grouped together, and we were inspired by some of the radial menus found in games; they navigate hierarchical options well, so I set out to implement a button that would produce that same explosive, radial effect, revealing a set of sub-option buttons.

RSExplodingButton Sample Animation
The Final Result - RSExplodingButton

Adding the Explode Animation

I’d initially used Core Animation to expand the button quickly, giving the illusion of explosive growth. With the right velocity and easing, it looked pretty good and for iOS 6 and earlier devices this would be fine. iOS 7 comes with a new animation - the spring animation. Its started making its way into a lot of apps, probably one of the most noticeable is the Rise alarm clock’s side-swipe. Whether Rise is using the new UIView spring animation or whether they’ve rolled their own, not sure. Either way, it adds a visceral feel to the control and I like that - when its done with subtlety.

Next I had to create the sub-buttons.

Creating Leaf Buttons

Think of the as a tree structure with limited depth. Each root button has n leaf buttons.

When the root button explodes, all of the leaf buttons needed to fan out in symmetrical directions an angle (360 / buttonID) that places their center at a distance of approximately (self.frame.size.width + leaf.frame.size.width + buffer). Here I’m thinking in polar coordinates, and I’ll need to translate that into cartesian:

CGFloat x = distance * cos(angle);
CGFloat y = distance * sin(angle);

A subtle but significant note: the leaf buttons won’t actually be drawn if they’re considered subviews of the root button because they exist outside of the root button’s bounds. We have to add the leaf buttons to the root button’s superview, below the root button.

This created a small issue with detecting the touches in leaf buttons: it wasn’t happening.

Handling Leaf Button Selection

A quick intro on how touches are handled: A hit-test is performed during a touch, beginning with the root view of the app delegate’s window. It drills upwards through all subviews until its at the top-most view in the hierarchy. When it finds that top-most view, it considers that view the first responder and attempts to resolve the touch event. If that view cannot handle an event, the event moves up the responder chain until a handler is found.

Also as noted in the Event Handling guide for iOS:

A touch object is associated with its hit-test view for its lifetime, even if the touch later moves outside the view.

So the problem with this button structure is that to reach a leaf button, the touch needs to leave the bounds of the root button, and somehow be intercepted by another button that is not a subview within the responder chain of the root button. Ouch.

Implementing the touchesMoved method, I would run a hit-test on each of the leaf buttons and test if they were an RSExplodingButton class. That got me detecting when I was over one of them.

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    for (UIControl *button in _leafButtons)
    {
        if([[self hitTest:[touch locationInView:button] withEvent:event] isKindOfClass:[RSExplodingButton class]])
        {
            NSLog(@"touch detected on button at index %d" [_leafButtons indexOfObject:button]);
        }
    }
    [self implodeButton];
}

I’m not sure I love the idea of iterating through all of the leaf buttons and conducting a hit-test, given that the original hit-test should tell me this information. It seems redundant, so I flagged it for a later look.

Handling Selection

Just like a typical UIButton, each button was going to have its own method that would be triggered on the touchUpInside event, and those methods can be handled as expected by the Controller handling the root button:

[leafButton addTarget:self action:@selector(onButtonXTap:) forControlEvents:UIControlEventTouchUpInside];

Now that we had the touch detection working, all that was left was to make sure the UIControlEvent was fired:

if([[self hitTest:[touch locationInView:button] withEvent:event] isKindOfClass:[RSExplodingButton class]])
{
    NSLog(@"touch detected on button at index %d" [_leafButtons indexOfObject:button]);
    [button sendActionsForControlEvents:UIControlEventTouchUpInside];
}

Finishing Touches

I’ll be adding some styling to the button so that by default it looks a bit like the iOS 7 lock-screen buttons, and add some visual indicators to differentiate between selected and non-selected buttons.

I’m pretty happy with how it turned out, and the control is quite responsive. I’m looking forward to seeing how users react to it and whether its an efficient and intuitive experience.

The source can be found up on my github, and I’d love feedback on it.