// // DWBubbleMenuButton.m // DWBubbleMenuButtonExample // // Created by Derrick Walker on 10/8/14. // Copyright (c) 2014 Derrick Walker. All rights reserved. // #import "DWBubbleMenuButton.h" #define kDefaultAnimationDuration 0.25f @interface DWBubbleMenuButton () @property (nonatomic, strong) UITapGestureRecognizer *tapGestureRecognizer; @property (nonatomic, strong) NSMutableArray *buttonContainer; @property (nonatomic, assign) CGRect originFrame; @end @implementation DWBubbleMenuButton #pragma mark - #pragma mark Public Methods - (void)addButtons:(NSArray *)buttons { assert(buttons != nil); for (UIButton *button in buttons) { [self addButton:button]; } if (self.homeButtonView != nil) { [self bringSubviewToFront:self.homeButtonView]; } } - (void)addButton:(UIButton *)button { assert(button != nil); if (_buttonContainer == nil) { self.buttonContainer = [[NSMutableArray alloc] init]; } if ([_buttonContainer containsObject:button] == false) { [_buttonContainer addObject:button]; [self addSubview:button]; button.hidden = YES; } } - (void)showButtons { if ([self.delegate respondsToSelector:@selector(bubbleMenuButtonWillExpand:)]) { [self.delegate bubbleMenuButtonWillExpand:self]; } [self _prepareForButtonExpansion]; self.userInteractionEnabled = NO; [CATransaction begin]; [CATransaction setAnimationDuration:_animationDuration]; [CATransaction setCompletionBlock:^{ for (UIButton *button in _buttonContainer) { button.transform = CGAffineTransformIdentity; } if (self.delegate != nil) { if ([self.delegate respondsToSelector:@selector(bubbleMenuButtonDidExpand:)]) { [self.delegate bubbleMenuButtonDidExpand:self]; } } self.userInteractionEnabled = YES; }]; NSArray *buttonContainer = _buttonContainer; if (self.direction == DirectionUp || self.direction == DirectionLeft) { buttonContainer = [self _reverseOrderFromArray:_buttonContainer]; } for (int i = 0; i < buttonContainer.count; i++) { int index = (int)buttonContainer.count - (i + 1); UIButton *button = [buttonContainer objectAtIndex:index]; button.hidden = NO; // position animation CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position"]; CGPoint originPosition = CGPointZero; CGPoint finalPosition = CGPointZero; switch (self.direction) { case DirectionLeft: originPosition = CGPointMake(self.frame.size.width - self.homeButtonView.frame.size.width, self.frame.size.height/2.f); finalPosition = CGPointMake(self.frame.size.width - self.homeButtonView.frame.size.width - button.frame.size.width/2.f - self.buttonSpacing - ((button.frame.size.width + self.buttonSpacing) * index), self.frame.size.height/2.f); break; case DirectionRight: originPosition = CGPointMake(self.homeButtonView.frame.size.width, self.frame.size.height/2.f); finalPosition = CGPointMake(self.homeButtonView.frame.size.width + self.buttonSpacing + button.frame.size.width/2.f + ((button.frame.size.width + self.buttonSpacing) * index), self.frame.size.height/2.f); break; case DirectionUp: originPosition = CGPointMake(self.frame.size.width/2.f, self.frame.size.height - self.homeButtonView.frame.size.height); finalPosition = CGPointMake(self.frame.size.width/2.f, self.frame.size.height - self.homeButtonView.frame.size.height - self.buttonSpacing - button.frame.size.height/2.f - ((button.frame.size.height + self.buttonSpacing) * index)); break; case DirectionDown: originPosition = CGPointMake(self.frame.size.width/2.f, self.homeButtonView.frame.size.height); finalPosition = CGPointMake(self.frame.size.width/2.f, self.homeButtonView.frame.size.height + self.buttonSpacing + button.frame.size.height/2.f + ((button.frame.size.height + self.buttonSpacing) * index)); break; default: break; } positionAnimation.duration = _animationDuration; positionAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; positionAnimation.fromValue = [NSValue valueWithCGPoint:originPosition]; positionAnimation.toValue = [NSValue valueWithCGPoint:finalPosition]; positionAnimation.beginTime = CACurrentMediaTime() + (_animationDuration/(float)_buttonContainer.count * (float)i); positionAnimation.fillMode = kCAFillModeForwards; positionAnimation.removedOnCompletion = NO; [button.layer addAnimation:positionAnimation forKey:@"positionAnimation"]; button.layer.position = finalPosition; // scale animation CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; scaleAnimation.duration = _animationDuration; scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; scaleAnimation.fromValue = [NSNumber numberWithFloat:0.01f]; scaleAnimation.toValue = [NSNumber numberWithFloat:1.f]; scaleAnimation.beginTime = CACurrentMediaTime() + (_animationDuration/(float)_buttonContainer.count * (float)i) + 0.03f; scaleAnimation.fillMode = kCAFillModeForwards; scaleAnimation.removedOnCompletion = NO; [button.layer addAnimation:scaleAnimation forKey:@"scaleAnimation"]; button.transform = CGAffineTransformMakeScale(0.01f, 0.01f); } [CATransaction commit]; _isCollapsed = NO; } - (void)dismissButtons { if ([self.delegate respondsToSelector:@selector(bubbleMenuButtonWillCollapse:)]) { [self.delegate bubbleMenuButtonWillCollapse:self]; } self.userInteractionEnabled = NO; [CATransaction begin]; [CATransaction setAnimationDuration:_animationDuration]; [CATransaction setCompletionBlock:^{ [self _finishCollapse]; for (UIButton *button in _buttonContainer) { button.transform = CGAffineTransformIdentity; button.hidden = YES; } if (self.delegate != nil) { if ([self.delegate respondsToSelector:@selector(bubbleMenuButtonDidCollapse:)]) { [self.delegate bubbleMenuButtonDidCollapse:self]; } } self.userInteractionEnabled = YES; }]; int index = 0; for (int i = (int)_buttonContainer.count - 1; i >= 0; i--) { UIButton *button = [_buttonContainer objectAtIndex:i]; if (self.direction == DirectionDown || self.direction == DirectionRight) { button = [_buttonContainer objectAtIndex:index]; } // scale animation CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; scaleAnimation.duration = _animationDuration; scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; scaleAnimation.fromValue = [NSNumber numberWithFloat:1.f]; scaleAnimation.toValue = [NSNumber numberWithFloat:0.01f]; scaleAnimation.beginTime = CACurrentMediaTime() + (_animationDuration/(float)_buttonContainer.count * (float)index) + 0.03; scaleAnimation.fillMode = kCAFillModeForwards; scaleAnimation.removedOnCompletion = NO; [button.layer addAnimation:scaleAnimation forKey:@"scaleAnimation"]; button.transform = CGAffineTransformMakeScale(1.f, 1.f); // position animation CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position"]; CGPoint originPosition = button.layer.position; CGPoint finalPosition = CGPointZero; switch (self.direction) { case DirectionLeft: finalPosition = CGPointMake(self.frame.size.width - self.homeButtonView.frame.size.width, self.frame.size.height/2.f); break; case DirectionRight: finalPosition = CGPointMake(self.homeButtonView.frame.size.width, self.frame.size.height/2.f); break; case DirectionUp: finalPosition = CGPointMake(self.frame.size.width/2.f, self.frame.size.height - self.homeButtonView.frame.size.height); break; case DirectionDown: finalPosition = CGPointMake(self.frame.size.width/2.f, self.homeButtonView.frame.size.height); break; default: break; } positionAnimation.duration = _animationDuration; positionAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; positionAnimation.fromValue = [NSValue valueWithCGPoint:originPosition]; positionAnimation.toValue = [NSValue valueWithCGPoint:finalPosition]; positionAnimation.beginTime = CACurrentMediaTime() + (_animationDuration/(float)_buttonContainer.count * (float)index); positionAnimation.fillMode = kCAFillModeForwards; positionAnimation.removedOnCompletion = NO; [button.layer addAnimation:positionAnimation forKey:@"positionAnimation"]; button.layer.position = originPosition; index++; } [CATransaction commit]; _isCollapsed = YES; } #pragma mark - #pragma mark Private Methods - (void)_defaultInit { self.clipsToBounds = YES; self.layer.masksToBounds = YES; self.direction = DirectionUp; self.animatedHighlighting = YES; self.collapseAfterSelection = YES; self.animationDuration = kDefaultAnimationDuration; self.standbyAlpha = 1.f; self.highlightAlpha = 0.45f; self.originFrame = self.frame; self.buttonSpacing = 20.f; _isCollapsed = YES; self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_handleTapGesture:)]; self.tapGestureRecognizer.cancelsTouchesInView = NO; self.tapGestureRecognizer.delegate = self; [self addGestureRecognizer:self.tapGestureRecognizer]; } - (void)_handleTapGesture:(id)sender { if (self.tapGestureRecognizer.state == UIGestureRecognizerStateEnded) { CGPoint touchLocation = [self.tapGestureRecognizer locationOfTouch:0 inView:self]; if (_collapseAfterSelection && _isCollapsed == NO && CGRectContainsPoint(self.homeButtonView.frame, touchLocation) == false) { [self dismissButtons]; } } } - (void)_animateWithBlock:(void (^)(void))animationBlock { [UIView transitionWithView:self duration:kDefaultAnimationDuration options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut animations:animationBlock completion:NULL]; } - (void)_setTouchHighlighted:(BOOL)highlighted { float alphaValue = highlighted ? _highlightAlpha : _standbyAlpha; if (self.homeButtonView.alpha == alphaValue) return; if (_animatedHighlighting) { [self _animateWithBlock:^{ if (self.homeButtonView != nil) { self.homeButtonView.alpha = alphaValue; } }]; } else { if (self.homeButtonView != nil) { self.homeButtonView.alpha = alphaValue; } } } - (float)_combinedButtonHeight { float height = 0; for (UIButton *button in _buttonContainer) { height += button.frame.size.height + self.buttonSpacing; } return height; } - (float)_combinedButtonWidth { float width = 0; for (UIButton *button in _buttonContainer) { width += button.frame.size.width + self.buttonSpacing; } return width; } - (void)_prepareForButtonExpansion { float buttonHeight = [self _combinedButtonHeight]; float buttonWidth = [self _combinedButtonWidth]; switch (self.direction) { case DirectionUp: { self.homeButtonView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin; CGRect frame = self.frame; frame.origin.y -= buttonHeight; frame.size.height += buttonHeight; self.frame = frame; } break; case DirectionDown: { self.homeButtonView.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin; CGRect frame = self.frame; frame.size.height += buttonHeight; self.frame = frame; } break; case DirectionLeft: { self.homeButtonView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; CGRect frame = self.frame; frame.origin.x -= buttonWidth; frame.size.width += buttonWidth; self.frame = frame; } break; case DirectionRight: { self.homeButtonView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; CGRect frame = self.frame; frame.size.width += buttonWidth; self.frame = frame; } break; default: break; } } - (void)_finishCollapse { self.frame = _originFrame; } - (UIView *)_subviewForPoint:(CGPoint)point { for (UIView *subview in self.subviews) { if (CGRectContainsPoint(subview.frame, point)) { return subview; } } return self; } - (NSArray *)_reverseOrderFromArray:(NSArray *)array { NSMutableArray *reverseArray = [NSMutableArray array]; for (int i = (int)array.count - 1; i >= 0; i--) { [reverseArray addObject:[array objectAtIndex:i]]; } return reverseArray; } #pragma mark - #pragma mark Setters/Getters - (void)setHomeButtonView:(UIView *)homeButtonView { if (_homeButtonView != homeButtonView) { _homeButtonView = homeButtonView; } if ([_homeButtonView isDescendantOfView:self] == NO) { [self addSubview:_homeButtonView]; } } - (NSArray *)buttons { return [_buttonContainer copy]; } #pragma mark - #pragma mark Touch Handling Methods - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; UITouch *touch = [touches anyObject]; if (CGRectContainsPoint(self.homeButtonView.frame, [touch locationInView:self])) { [self _setTouchHighlighted:YES]; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; UITouch *touch = [touches anyObject]; [self _setTouchHighlighted:NO]; if (CGRectContainsPoint(self.homeButtonView.frame, [touch locationInView:self])) { if (_isCollapsed) { [self showButtons]; } else { [self dismissButtons]; } } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; [self _setTouchHighlighted:NO]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; UITouch *touch = [touches anyObject]; [self _setTouchHighlighted:CGRectContainsPoint(self.homeButtonView.frame, [touch locationInView:self])]; } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hitView = [super hitTest:point withEvent:event]; if (hitView == self) { if (_isCollapsed) { return self; } else { return [self _subviewForPoint:point]; } } return hitView; } #pragma mark - #pragma mark UIGestureRecognizer Delegate - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { CGPoint touchLocation = [touch locationInView:self]; if ([self _subviewForPoint:touchLocation] != self && _collapseAfterSelection) { return YES; } return NO; } #pragma mark - #pragma mark Lifecycle - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self _defaultInit]; } return self; } - (id)initWithFrame:(CGRect)frame expansionDirection:(ExpansionDirection)direction { self = [super initWithFrame:frame]; if (self) { [self _defaultInit]; _direction = direction; } return self; } @end