//
|
// AMPopTip.m
|
// PopTipDemo
|
//
|
// Created by Andrea Mazzini on 11/07/14.
|
// Copyright (c) 2014 Fancy Pixel. All rights reserved.
|
//
|
|
#import "AMPopTip.h"
|
#import "AMPopTipDefaults.h"
|
#import "AMPopTip+Draw.h"
|
#import "AMPopTip+Entrance.h"
|
|
#import "AMPopTip+Animation.h"
|
|
#import <objc/runtime.h>
|
|
@interface AMPopTip()
|
|
@property (nonatomic, strong) NSString *text;
|
@property (nonatomic, strong) NSAttributedString *attributedText;
|
@property (nonatomic, strong) NSMutableParagraphStyle *paragraphStyle;
|
@property (nonatomic, strong) UITapGestureRecognizer *gestureRecognizer;
|
@property (nonatomic, strong) UITapGestureRecognizer *tapRemoveGesture;
|
@property (nonatomic, strong) UISwipeGestureRecognizer *swipeRemoveGesture;
|
@property (nonatomic, strong) NSTimer *dismissTimer;
|
@property (nonatomic, weak, readwrite) UIView *containerView;
|
@property (nonatomic, assign, readwrite) AMPopTipDirection direction;
|
@property (nonatomic, assign, readwrite) CGPoint arrowPosition;
|
@property (nonatomic, assign, readwrite) BOOL isVisible;
|
@property (nonatomic, assign, readwrite) BOOL isAnimating;
|
@property (nonatomic, assign) CGRect textBounds;
|
@property (nonatomic, assign) CGFloat maxWidth;
|
@property (nonatomic, strong) UIView *customView;
|
|
@end
|
|
#define DEGREES_TO_RADIANS(degrees) ((3.14159265359 * degrees)/ 180)
|
|
@implementation AMPopTip
|
|
+ (instancetype)popTip {
|
return [[AMPopTip alloc] init];
|
}
|
|
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
self = [super initWithCoder:aDecoder];
|
if (self) {
|
[self commonInit];
|
}
|
return self;
|
}
|
|
- (instancetype)initWithFrame:(CGRect)ignoredFrame {
|
self = [super initWithFrame:CGRectZero];
|
if (self) {
|
[self commonInit];
|
}
|
return self;
|
}
|
|
- (instancetype)init {
|
self = [super initWithFrame:CGRectZero];
|
if (self) {
|
[self commonInit];
|
}
|
return self;
|
}
|
|
- (void)commonInit {
|
_paragraphStyle = [[NSMutableParagraphStyle alloc] init];
|
_textAlignment = NSTextAlignmentCenter;
|
_font = kDefaultFont;
|
_textColor = kDefaultTextColor;
|
_popoverColor = kDefaultBackgroundColor;
|
_borderColor = kDefaultBorderColor;
|
_borderWidth = kDefaultBorderWidth;
|
_radius = kDefaultRadius;
|
_padding = kDefaultPadding;
|
_arrowSize = kDefaultArrowSize;
|
_animationIn = kDefaultAnimationIn;
|
_animationOut = kDefaultAnimationOut;
|
_isVisible = NO;
|
_shouldDismissOnTapOutside = YES;
|
_edgeMargin = kDefaultEdgeMargin;
|
_edgeInsets = kDefaultEdgeInsets;
|
_rounded = NO;
|
_offset = kDefaultOffset;
|
_entranceAnimation = AMPopTipEntranceAnimationScale;
|
_actionAnimation = AMPopTipActionAnimationNone;
|
_actionFloatOffset = kDefaultFloatOffset;
|
_actionBounceOffset = kDefaultBounceOffset;
|
_actionPulseOffset = kDefaultPulseOffset;
|
_actionAnimationIn = kDefaultBounceAnimationIn;
|
_actionAnimationOut = kDefaultBounceAnimationOut;
|
_tapRemoveGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapRemoveGestureHandler)];
|
_swipeRemoveGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRemoveGestureHandler)];
|
}
|
|
- (void)layoutSubviews {
|
[self setup];
|
}
|
|
- (void)setup {
|
if (self.direction == AMPopTipDirectionLeft) {
|
self.maxWidth = MIN(self.maxWidth, self.fromFrame.origin.x - self.padding * 2 - self.edgeInsets.left - self.edgeInsets.right - self.arrowSize.width);
|
}
|
if (self.direction == AMPopTipDirectionRight) {
|
self.maxWidth = MIN(self.maxWidth, self.containerView.bounds.size.width - self.fromFrame.origin.x - self.fromFrame.size.width - self.padding * 2 - self.edgeInsets.left - self.edgeInsets.right - self.arrowSize.width);
|
}
|
|
if (self.text != nil) {
|
self.textBounds = [self.text boundingRectWithSize:(CGSize){self.maxWidth, DBL_MAX }
|
options:NSStringDrawingUsesLineFragmentOrigin
|
attributes:@{NSFontAttributeName: self.font}
|
context:nil];
|
} else if (self.attributedText != nil) {
|
self.textBounds = [self.attributedText boundingRectWithSize:(CGSize){self.maxWidth, DBL_MAX }
|
options:NSStringDrawingUsesLineFragmentOrigin
|
context:nil];
|
} else if (self.customView != nil) {
|
self.textBounds = self.customView.frame;
|
}
|
|
_textBounds.origin = (CGPoint){self.padding + self.edgeInsets.left, self.padding + self.edgeInsets.top};
|
|
CGRect frame = CGRectZero;
|
float offset = self.offset * ((self.direction == AMPopTipDirectionUp || self.direction == AMPopTipDirectionLeft || self.direction == AMPopTipDirectionNone) ? -1 : 1);
|
|
if (self.direction == AMPopTipDirectionUp || self.direction == AMPopTipDirectionDown) {
|
frame.size = (CGSize){self.textBounds.size.width + self.padding * 2.0 + self.edgeInsets.left + self.edgeInsets.right, self.textBounds.size.height + self.padding * 2.0 + self.edgeInsets.top + self.edgeInsets.bottom + self.arrowSize.height};
|
|
CGFloat x = self.fromFrame.origin.x + self.fromFrame.size.width / 2 - frame.size.width / 2;
|
if (x < 0) { x = self.edgeMargin; }
|
if (x + frame.size.width > self.containerView.bounds.size.width) { x = self.containerView.bounds.size.width - frame.size.width - self.edgeMargin; }
|
if (self.direction == AMPopTipDirectionDown) {
|
frame.origin = (CGPoint){ x, self.fromFrame.origin.y + self.fromFrame.size.height };
|
} else {
|
frame.origin = (CGPoint){ x, self.fromFrame.origin.y - frame.size.height};
|
}
|
|
frame.origin.y += offset;
|
|
} else if (self.direction == AMPopTipDirectionLeft || self.direction == AMPopTipDirectionRight) {
|
frame.size = (CGSize){ self.textBounds.size.width + self.padding * 2.0 + self.edgeInsets.left + self.edgeInsets.right + self.arrowSize.height, self.textBounds.size.height + self.padding * 2.0 + self.edgeInsets.top + self.edgeInsets.bottom};
|
|
CGFloat x = 0;
|
if (self.direction == AMPopTipDirectionLeft) {
|
x = self.fromFrame.origin.x - frame.size.width;
|
}
|
if (self.direction == AMPopTipDirectionRight) {
|
x = self.fromFrame.origin.x + self.fromFrame.size.width;
|
}
|
|
x += offset;
|
|
CGFloat y = self.fromFrame.origin.y + self.fromFrame.size.height / 2 - frame.size.height / 2;
|
|
if (y < 0) { y = self.edgeMargin; }
|
if (y + frame.size.height > self.containerView.bounds.size.height) { y = self.containerView.bounds.size.height - frame.size.height - self.edgeMargin; }
|
frame.origin = (CGPoint){ x, y };
|
} else {
|
frame.size = (CGSize){ self.textBounds.size.width + self.padding * 2.0 + self.edgeInsets.left + self.edgeInsets.right, self.textBounds.size.height + self.padding * 2.0 + self.edgeInsets.top + self.edgeInsets.bottom };
|
frame.origin = (CGPoint){ CGRectGetMidX(self.fromFrame) - frame.size.width / 2, CGRectGetMidY(self.fromFrame) - frame.size.height / 2 + offset };
|
}
|
|
frame.size = (CGSize){ frame.size.width + self.borderWidth * 2, frame.size.height + self.borderWidth * 2 };
|
|
switch (self.direction) {
|
case AMPopTipDirectionNone: {
|
self.arrowPosition = CGPointZero;
|
self.layer.anchorPoint = (CGPoint){ 0.5, 0.5 };
|
self.layer.position = (CGPoint){ CGRectGetMidX(self.fromFrame), CGRectGetMidY(self.fromFrame) };
|
break;
|
}
|
case AMPopTipDirectionDown: {
|
self.arrowPosition = (CGPoint){
|
self.fromFrame.origin.x + self.fromFrame.size.width / 2 - frame.origin.x,
|
self.fromFrame.origin.y + self.fromFrame.size.height - frame.origin.y + offset
|
};
|
CGFloat anchor = self.arrowPosition.x / frame.size.width;
|
_textBounds.origin = (CGPoint){ self.textBounds.origin.x, self.textBounds.origin.y + self.arrowSize.height };
|
self.layer.anchorPoint = (CGPoint){ anchor, 0 };
|
self.layer.position = (CGPoint){ self.layer.position.x + frame.size.width * anchor, self.layer.position.y - frame.size.height / 2 };
|
|
break;
|
}
|
case AMPopTipDirectionUp: {
|
self.arrowPosition = (CGPoint){
|
self.fromFrame.origin.x + self.fromFrame.size.width / 2 - frame.origin.x,
|
frame.size.height
|
};
|
CGFloat anchor = self.arrowPosition.x / frame.size.width;
|
self.layer.anchorPoint = (CGPoint){ anchor, 1 };
|
self.layer.position = (CGPoint){ self.layer.position.x + frame.size.width * anchor, self.layer.position.y + frame.size.height / 2 };
|
|
break;
|
}
|
case AMPopTipDirectionLeft: {
|
self.arrowPosition = (CGPoint){
|
self.fromFrame.origin.x - frame.origin.x + offset,
|
self.fromFrame.origin.y + self.fromFrame.size.height / 2 - frame.origin.y
|
};
|
CGFloat anchor = self.arrowPosition.y / frame.size.height;
|
self.layer.anchorPoint = (CGPoint){ 1, anchor };
|
self.layer.position = (CGPoint){ self.layer.position.x - frame.size.width / 2, self.layer.position.y + frame.size.height * anchor };
|
|
break;
|
}
|
case AMPopTipDirectionRight: {
|
self.arrowPosition = (CGPoint){
|
self.fromFrame.origin.x + self.fromFrame.size.width - frame.origin.x + offset,
|
self.fromFrame.origin.y + self.fromFrame.size.height / 2 - frame.origin.y
|
};
|
_textBounds.origin = (CGPoint){ self.textBounds.origin.x + self.arrowSize.height, self.textBounds.origin.y };
|
CGFloat anchor = self.arrowPosition.y / frame.size.height;
|
self.layer.anchorPoint = (CGPoint){ 0, anchor };
|
self.layer.position = (CGPoint){ self.layer.position.x + frame.size.width / 2, self.layer.position.y + frame.size.height * anchor };
|
|
break;
|
}
|
}
|
|
self.backgroundColor = [UIColor clearColor];
|
self.frame = frame;
|
|
if (self.customView) {
|
self.customView.frame = self.textBounds;
|
}
|
|
self.gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
[self addGestureRecognizer:self.gestureRecognizer];
|
[self setNeedsDisplay];
|
}
|
|
- (void)handleTap:(UITapGestureRecognizer *)gesture {
|
if (self.shouldDismissOnTap) {
|
[self hide];
|
}
|
if (self.tapHandler) {
|
self.tapHandler();
|
}
|
}
|
|
- (void)tapRemoveGestureHandler {
|
if (self.shouldDismissOnTapOutside) {
|
[self hide];
|
}
|
}
|
|
- (void)swipeRemoveGestureHandler {
|
if (self.shouldDismissOnSwipeOutside) {
|
[self hide];
|
}
|
}
|
|
- (void)drawRect:(CGRect)rect {
|
if (self.isRounded) {
|
BOOL showHorizontally = self.direction == AMPopTipDirectionLeft || self.direction == AMPopTipDirectionRight;
|
self.radius = (self.frame.size.height - (showHorizontally ? 0 : self.arrowSize.height)) / 2 ;
|
}
|
|
UIBezierPath *path = [self pathWithRect:rect direction:self.direction];
|
|
[self.popoverColor setFill];
|
[path fill];
|
|
[self.borderColor setStroke];
|
[path setLineWidth:self.borderWidth];
|
[path stroke];
|
|
self.paragraphStyle.alignment = self.textAlignment;
|
|
NSDictionary *titleAttributes = @{
|
NSParagraphStyleAttributeName: self.paragraphStyle,
|
NSFontAttributeName: self.font,
|
NSForegroundColorAttributeName: self.textColor
|
};
|
|
if (self.text != nil) {
|
[self.text drawInRect:self.textBounds withAttributes:titleAttributes];
|
} else if (self.attributedText != nil) {
|
[self.attributedText drawInRect:self.textBounds];
|
}
|
}
|
|
- (void)show {
|
if (self.isVisible || self.isAnimating) {
|
return;
|
}
|
self.isAnimating = YES;
|
[self setNeedsLayout];
|
[self performEntranceAnimation:^{
|
[self.containerView addGestureRecognizer:self.tapRemoveGesture];
|
[self.containerView addGestureRecognizer:self.swipeRemoveGesture];
|
if (self.appearHandler) {
|
self.appearHandler();
|
}
|
if (self.actionAnimation != AMPopTipActionAnimationNone) {
|
[self startActionAnimation];
|
}
|
self.isVisible = YES;
|
self.isAnimating = NO;
|
}];
|
}
|
|
- (void)showText:(NSString *)text direction:(AMPopTipDirection)direction maxWidth:(CGFloat)maxWidth inView:(UIView *)view fromFrame:(CGRect)frame {
|
self.attributedText = nil;
|
self.text = text;
|
self.accessibilityLabel = text;
|
self.direction = direction;
|
self.containerView = view;
|
self.maxWidth = maxWidth;
|
_fromFrame = frame;
|
self.customView = nil;
|
|
[self show];
|
}
|
|
- (void)showAttributedText:(NSAttributedString *)text direction:(AMPopTipDirection)direction maxWidth:(CGFloat)maxWidth inView:(UIView *)view fromFrame:(CGRect)frame {
|
self.text = nil;
|
self.attributedText = text;
|
self.accessibilityLabel = [text string];
|
self.direction = direction;
|
self.containerView = view;
|
self.maxWidth = maxWidth;
|
_fromFrame = frame;
|
self.customView = nil;
|
|
[self show];
|
}
|
|
- (void)showCustomView:(UIView *)customView direction:(AMPopTipDirection)direction inView:(UIView *)view fromFrame:(CGRect)frame {
|
self.text = nil;
|
self.attributedText = nil;
|
self.direction = direction;
|
self.containerView = view;
|
self.maxWidth = customView.frame.size.width;
|
_fromFrame = frame;
|
self.customView = customView;
|
|
[self addSubview:self.customView];
|
|
[self show];
|
}
|
|
- (void)setFromFrame:(CGRect)fromFrame {
|
_fromFrame = fromFrame;
|
[self setup];
|
}
|
|
- (void)showText:(NSString *)text direction:(AMPopTipDirection)direction maxWidth:(CGFloat)maxWidth inView:(UIView *)view fromFrame:(CGRect)frame duration:(NSTimeInterval)interval {
|
[self showText:text direction:direction maxWidth:maxWidth inView:view fromFrame:frame];
|
[self.dismissTimer invalidate];
|
if (interval > 0) {
|
self.dismissTimer = [NSTimer scheduledTimerWithTimeInterval:interval
|
target:self
|
selector:@selector(hide)
|
userInfo:nil
|
repeats:NO];
|
}
|
}
|
|
- (void)showAttributedText:(NSAttributedString *)text direction:(AMPopTipDirection)direction maxWidth:(CGFloat)maxWidth inView:(UIView *)view fromFrame:(CGRect)frame duration:(NSTimeInterval)interval {
|
[self showAttributedText:text direction:direction maxWidth:maxWidth inView:view fromFrame:frame];
|
[self.dismissTimer invalidate];
|
if (interval > 0){
|
self.dismissTimer = [NSTimer scheduledTimerWithTimeInterval:interval
|
target:self
|
selector:@selector(hide)
|
userInfo:nil
|
repeats:NO];
|
}
|
}
|
|
- (void)showCustomView:(UIView *)customView direction:(AMPopTipDirection)direction inView:(UIView *)view fromFrame:(CGRect)frame duration:(NSTimeInterval)interval {
|
[self showCustomView:customView direction:direction inView:view fromFrame:frame];
|
[self.dismissTimer invalidate];
|
if (interval > 0){
|
self.dismissTimer = [NSTimer scheduledTimerWithTimeInterval:interval
|
target:self
|
selector:@selector(hide)
|
userInfo:nil
|
repeats:NO];
|
}
|
}
|
|
- (void)hide {
|
if (!self.isVisible || self.isAnimating) {
|
return;
|
}
|
self.isAnimating = YES;
|
[self.dismissTimer invalidate];
|
self.dismissTimer = nil;
|
[self.containerView removeGestureRecognizer:self.tapRemoveGesture];
|
[self.containerView removeGestureRecognizer:self.swipeRemoveGesture];
|
if (self.superview) {
|
self.transform = CGAffineTransformIdentity;
|
[UIView animateWithDuration:self.animationOut delay:self.delayOut options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState) animations:^{
|
self.transform = CGAffineTransformMakeScale(0.000001, 0.000001);
|
} completion:^(BOOL finished) {
|
[self.customView removeFromSuperview];
|
[self stopActionAnimation];
|
[self removeFromSuperview];
|
[self.layer removeAllAnimations];
|
self.transform = CGAffineTransformIdentity;
|
self->_isVisible = NO;
|
self->_isAnimating = NO;
|
if (self.dismissHandler) {
|
self.dismissHandler();
|
}
|
}];
|
}
|
}
|
|
- (void)updateText:(NSString *)text {
|
self.text = text;
|
self.accessibilityLabel = text;
|
[self setNeedsLayout];
|
}
|
|
- (void)startActionAnimation {
|
[self performActionAnimation];
|
}
|
|
- (void)stopActionAnimation {
|
[self dismissActionAnimation];
|
}
|
|
- (void)setShouldDismissOnTapOutside:(BOOL)shouldDismissOnTapOutside {
|
_shouldDismissOnTapOutside = shouldDismissOnTapOutside;
|
_tapRemoveGesture.enabled = shouldDismissOnTapOutside;
|
}
|
|
- (void)setShouldDismissOnSwipeOutside:(BOOL)shouldDismissOnSwipeOutside {
|
_shouldDismissOnSwipeOutside = shouldDismissOnSwipeOutside;
|
_swipeRemoveGesture.enabled = shouldDismissOnSwipeOutside;
|
}
|
|
- (void)setSwipeRemoveGestureDirection:(UISwipeGestureRecognizerDirection)swipeRemoveGestureDirection {
|
_swipeRemoveGestureDirection = swipeRemoveGestureDirection;
|
_swipeRemoveGesture.direction = swipeRemoveGestureDirection;
|
}
|
|
- (void)dealloc {
|
[_tapRemoveGesture removeTarget:self action:@selector(tapRemoveGestureHandler)];
|
_tapRemoveGesture = nil;
|
|
[_swipeRemoveGesture removeTarget:self action:@selector(swipeRemoveGestureHandler)];
|
_swipeRemoveGesture = nil;
|
}
|
|
|
- (void)performEntranceAnimation:(void (^)())completion {
|
switch (self.entranceAnimation) {
|
case AMPopTipEntranceAnimationScale: {
|
[self entranceScale:completion];
|
break;
|
}
|
case AMPopTipEntranceAnimationTransition: {
|
[self entranceTransition:completion];
|
break;
|
}
|
case AMPopTipEntranceAnimationCustom: {
|
[self.containerView addSubview:self];
|
if (self.entranceAnimationHandler) {
|
self.entranceAnimationHandler(^{
|
completion();
|
});
|
}
|
}
|
case AMPopTipEntranceAnimationNone: {
|
[self.containerView addSubview:self];
|
completion();
|
break;
|
}
|
default: {
|
[self.containerView addSubview:self];
|
completion();
|
break;
|
}
|
}
|
}
|
|
- (void)entranceTransition:(void (^)())completion {
|
self.transform = CGAffineTransformMakeScale(0.6, 0.6);
|
switch (self.direction) {
|
case AMPopTipDirectionUp:
|
self.transform = CGAffineTransformTranslate(self.transform, 0, -self.fromFrame.origin.y);
|
break;
|
case AMPopTipDirectionDown:
|
self.transform = CGAffineTransformTranslate(self.transform, 0, (self.containerView.frame.size.height - self.fromFrame.origin.y));
|
break;
|
case AMPopTipDirectionLeft:
|
self.transform = CGAffineTransformTranslate(self.transform, -self.fromFrame.origin.x, 0);
|
break;
|
case AMPopTipDirectionRight:
|
self.transform = CGAffineTransformTranslate(self.transform, (self.containerView.frame.size.width - self.fromFrame.origin.x), 0);
|
break;
|
case AMPopTipDirectionNone:
|
self.transform = CGAffineTransformTranslate(self.transform, 0, (self.containerView.frame.size.height - self.fromFrame.origin.y));
|
break;
|
|
default:
|
break;
|
}
|
[self.containerView addSubview:self];
|
|
[UIView animateWithDuration:self.animationIn delay:self.delayIn usingSpringWithDamping:0.6 initialSpringVelocity:1.5 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState) animations:^{
|
self.transform = CGAffineTransformIdentity;
|
} completion:^(BOOL completed){
|
if (completed) {
|
completion();
|
}
|
}];
|
}
|
|
- (void)entranceScale:(void (^)())completion {
|
self.transform = CGAffineTransformMakeScale(0, 0);
|
[self.containerView addSubview:self];
|
|
[UIView animateWithDuration:self.animationIn delay:self.delayIn usingSpringWithDamping:0.6 initialSpringVelocity:1.5 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState) animations:^{
|
self.transform = CGAffineTransformIdentity;
|
} completion:^(BOOL completed){
|
if (completed) {
|
completion();
|
}
|
}];
|
}
|
|
- (UIBezierPath *)pathWithRect:(CGRect)rect direction:(AMPopTipDirection)direction {
|
UIBezierPath *path = [[UIBezierPath alloc] init];
|
CGRect baloonFrame;
|
|
// Drawing a round rect and the arrow alone sometime shows a white halfpixel line, so here's a fun bit of code... feel free to fall asleep
|
switch (direction) {
|
case AMPopTipDirectionNone: {
|
baloonFrame = (CGRect){ (CGPoint) { self.borderWidth, self.borderWidth }, (CGSize){ self.frame.size.width - self.borderWidth * 2, self.frame.size.height - self.borderWidth * 2} };
|
path = [UIBezierPath bezierPathWithRoundedRect:baloonFrame cornerRadius:self.radius];
|
|
break;
|
}
|
case AMPopTipDirectionDown: {
|
baloonFrame = (CGRect){ (CGPoint) { 0, self.arrowSize.height }, (CGSize){ rect.size.width - self.borderWidth * 2, rect.size.height - self.arrowSize.height - self.borderWidth * 2} };
|
|
[path moveToPoint:(CGPoint){ self.arrowPosition.x + self.borderWidth, self.arrowPosition.y }];
|
[path addLineToPoint:(CGPoint){ self.borderWidth + self.arrowPosition.x + self.arrowSize.width / 2, self.arrowPosition.y + self.arrowSize.height }];
|
[path addLineToPoint:(CGPoint){ baloonFrame.size.width - self.radius, self.arrowSize.height }];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.size.width - self.radius, self.arrowSize.height + self.radius } radius:self.radius startAngle:DEGREES_TO_RADIANS(270) endAngle:DEGREES_TO_RADIANS(0) clockwise:YES];
|
[path addLineToPoint:(CGPoint){ baloonFrame.size.width, self.arrowSize.height + baloonFrame.size.height - self.radius }];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.size.width - self.radius, self.arrowSize.height + baloonFrame.size.height - self.radius } radius:self.radius startAngle:DEGREES_TO_RADIANS(0) endAngle:DEGREES_TO_RADIANS(90) clockwise:YES];
|
[path addLineToPoint:(CGPoint){ self.borderWidth + self.radius, self.arrowSize.height + baloonFrame.size.height }];
|
[path addArcWithCenter:(CGPoint){ self.borderWidth + self.radius, self.arrowSize.height + baloonFrame.size.height - self.radius } radius:self.radius startAngle:DEGREES_TO_RADIANS(90) endAngle:DEGREES_TO_RADIANS(180) clockwise:YES];
|
[path addLineToPoint:(CGPoint){ self.borderWidth, self.arrowSize.height + self.radius }];
|
[path addArcWithCenter:(CGPoint){ self.borderWidth + self.radius, self.arrowSize.height + self.radius } radius:self.radius startAngle:DEGREES_TO_RADIANS(180) endAngle:DEGREES_TO_RADIANS(270) clockwise:YES];
|
[path addLineToPoint:(CGPoint){ self.borderWidth + self.arrowPosition.x - self.arrowSize.width / 2, self.arrowPosition.y + self.arrowSize.height }];
|
[path closePath];
|
|
break;
|
}
|
case AMPopTipDirectionUp: {
|
baloonFrame = (CGRect){ (CGPoint) { 0, 0 }, (CGSize){ rect.size.width - self.borderWidth * 2, rect.size.height - self.arrowSize.height - self.borderWidth * 2 } };
|
|
[path moveToPoint:(CGPoint){ self.arrowPosition.x + self.borderWidth, self.arrowPosition.y - self.borderWidth }];
|
[path addLineToPoint:(CGPoint){ self.borderWidth + self.arrowPosition.x + self.arrowSize.width / 2, self.arrowPosition.y - self.arrowSize.height - self.borderWidth }];
|
[path addLineToPoint:(CGPoint){ baloonFrame.size.width - self.radius, baloonFrame.origin.y + baloonFrame.size.height + self.borderWidth }];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.size.width - self.radius, baloonFrame.origin.y + baloonFrame.size.height - self.radius + self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(90) endAngle:DEGREES_TO_RADIANS(0) clockwise:NO];
|
[path addLineToPoint:(CGPoint){ baloonFrame.size.width, baloonFrame.origin.y + self.radius + self.borderWidth }];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.size.width - self.radius, baloonFrame.origin.y + self.radius + self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(0) endAngle:DEGREES_TO_RADIANS(270) clockwise:NO];
|
[path addLineToPoint:(CGPoint){ self.borderWidth + self.radius, baloonFrame.origin.y + self.borderWidth }];
|
[path addArcWithCenter:(CGPoint){ self.borderWidth + self.radius, baloonFrame.origin.y + self.radius + self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(270) endAngle:DEGREES_TO_RADIANS(180) clockwise:NO];
|
[path addLineToPoint:(CGPoint){ self.borderWidth, baloonFrame.origin.y + baloonFrame.size.height - self.radius + self.borderWidth }];
|
[path addArcWithCenter:(CGPoint){ self.borderWidth + self.radius, baloonFrame.origin.y + baloonFrame.size.height - self.radius + self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(180) endAngle:DEGREES_TO_RADIANS(90) clockwise:NO];
|
[path addLineToPoint:(CGPoint){ self.borderWidth + self.arrowPosition.x - self.arrowSize.width / 2, self.arrowPosition.y - self.arrowSize.height - self.borderWidth }];
|
[path closePath];
|
|
break;
|
}
|
case AMPopTipDirectionLeft: {
|
// Flip the size around for the left/right poptip
|
CGSize arrowSize = CGSizeMake(self.arrowSize.height, self.arrowSize.width);
|
baloonFrame = (CGRect){ (CGPoint) { 0, 0 }, (CGSize){ rect.size.width - arrowSize.width - self.borderWidth * 2, rect.size.height - self.borderWidth * 2} };
|
|
[path moveToPoint:(CGPoint){ self.arrowPosition.x - self.borderWidth, self.arrowPosition.y }];
|
[path addLineToPoint:(CGPoint){ self.arrowPosition.x - arrowSize.width - self.borderWidth, self.arrowPosition.y - arrowSize.height / 2 }];
|
[path addLineToPoint:(CGPoint){ baloonFrame.size.width - self.borderWidth, baloonFrame.origin.y + self.radius }];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.size.width - self.radius - self.borderWidth, baloonFrame.origin.y + self.radius + self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(0) endAngle:DEGREES_TO_RADIANS(270) clockwise:NO];
|
[path addLineToPoint:(CGPoint){ self.radius + self.borderWidth, baloonFrame.origin.y + self.borderWidth}];
|
[path addArcWithCenter:(CGPoint){ self.radius + self.borderWidth, baloonFrame.origin.y + self.radius + self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(270) endAngle:DEGREES_TO_RADIANS(180) clockwise:NO];
|
[path addLineToPoint:(CGPoint){ self.borderWidth, baloonFrame.origin.y + baloonFrame.size.height - self.radius - self.borderWidth }];
|
[path addArcWithCenter:(CGPoint){ self.radius + self.borderWidth, baloonFrame.origin.y + baloonFrame.size.height - self.radius - self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(180) endAngle:DEGREES_TO_RADIANS(90) clockwise:NO];
|
[path addLineToPoint:(CGPoint){ baloonFrame.size.width - self.radius - self.borderWidth, baloonFrame.origin.y + baloonFrame.size.height - self.borderWidth }];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.size.width - self.radius - self.borderWidth, baloonFrame.origin.y + baloonFrame.size.height - self.radius - self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(90) endAngle:DEGREES_TO_RADIANS(0) clockwise:NO];
|
[path addLineToPoint:(CGPoint){ self.arrowPosition.x - arrowSize.width - self.borderWidth, self.arrowPosition.y + arrowSize.height / 2 }];
|
[path closePath];
|
|
break;
|
}
|
case AMPopTipDirectionRight: {
|
// Flip the size around for the left/right poptip
|
CGSize arrowSize = CGSizeMake(self.arrowSize.height, self.arrowSize.width);
|
baloonFrame = (CGRect){ (CGPoint) { arrowSize.width, 0 }, (CGSize){ rect.size.width - arrowSize.width - self.borderWidth * 2, rect.size.height - self.borderWidth * 2} };
|
|
[path moveToPoint:(CGPoint){ self.arrowPosition.x + self.borderWidth, self.arrowPosition.y }];
|
[path addLineToPoint:(CGPoint){ self.arrowPosition.x + arrowSize.width + self.borderWidth, self.arrowPosition.y - arrowSize.height / 2 }];
|
[path addLineToPoint:(CGPoint){ baloonFrame.origin.x + self.borderWidth, baloonFrame.origin.y + self.radius + self.borderWidth }];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.origin.x + self.radius + self.borderWidth, baloonFrame.origin.y + self.radius + self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(180) endAngle:DEGREES_TO_RADIANS(270) clockwise:YES];
|
[path addLineToPoint:(CGPoint){ baloonFrame.origin.x + baloonFrame.size.width - self.radius - self.borderWidth, baloonFrame.origin.y + self.borderWidth}];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.origin.x + baloonFrame.size.width - self.radius - self.borderWidth, baloonFrame.origin.y + self.radius + self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(270) endAngle:DEGREES_TO_RADIANS(0) clockwise:YES];
|
[path addLineToPoint:(CGPoint){ baloonFrame.origin.x + baloonFrame.size.width - self.borderWidth, baloonFrame.origin.y + baloonFrame.size.height - self.radius - self.borderWidth }];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.origin.x + baloonFrame.size.width - self.radius - self.borderWidth, baloonFrame.origin.y + baloonFrame.size.height - self.radius - self.borderWidth} radius:self.radius startAngle:DEGREES_TO_RADIANS(0) endAngle:DEGREES_TO_RADIANS(90) clockwise:YES];
|
[path addLineToPoint:(CGPoint){ baloonFrame.origin.x + self.radius + self.borderWidth, baloonFrame.origin.y + baloonFrame.size.height - self.borderWidth}];
|
[path addArcWithCenter:(CGPoint){ baloonFrame.origin.x + self.radius + self.borderWidth, baloonFrame.origin.y + baloonFrame.size.height - self.radius - self.borderWidth } radius:self.radius startAngle:DEGREES_TO_RADIANS(90) endAngle:DEGREES_TO_RADIANS(180) clockwise:YES];
|
[path addLineToPoint:(CGPoint){ self.arrowPosition.x + arrowSize.width + self.borderWidth, self.arrowPosition.y + arrowSize.height / 2 }];
|
[path closePath];
|
|
break;
|
}
|
}
|
return path;
|
}
|
|
- (void)setShouldBounce:(BOOL)shouldBounce { objc_setAssociatedObject(self, @selector(shouldBounce), [NSNumber numberWithBool:shouldBounce], OBJC_ASSOCIATION_RETAIN);}
|
- (BOOL)shouldBounce { return [objc_getAssociatedObject(self, @selector(shouldBounce)) boolValue]; }
|
|
- (void)performActionAnimation {
|
switch (self.actionAnimation) {
|
case AMPopTipActionAnimationBounce:
|
self.shouldBounce = YES;
|
[self bounceAnimation];
|
break;
|
case AMPopTipActionAnimationFloat:
|
[self floatAnimation];
|
break;
|
case AMPopTipActionAnimationPulse:
|
[self pulseAnimation];
|
break;
|
case AMPopTipActionAnimationNone:
|
return;
|
break;
|
default:
|
break;
|
}
|
}
|
|
- (void)floatAnimation {
|
CGFloat xOffset = 0;
|
CGFloat yOffset = 0;
|
switch (self.direction) {
|
case AMPopTipDirectionUp:
|
yOffset = -self.actionFloatOffset;
|
break;
|
case AMPopTipDirectionDown:
|
yOffset = self.actionFloatOffset;
|
break;
|
case AMPopTipDirectionLeft:
|
xOffset = -self.actionFloatOffset;
|
break;
|
case AMPopTipDirectionRight:
|
xOffset = self.actionFloatOffset;
|
break;
|
case AMPopTipDirectionNone:
|
yOffset = -self.actionFloatOffset;
|
break;
|
}
|
|
[UIView animateWithDuration:(self.actionAnimationIn / 2) delay:self.actionDelayIn options:(UIViewAnimationOptionRepeat | UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAutoreverse | UIViewAnimationOptionAllowUserInteraction) animations:^{
|
self.transform = CGAffineTransformMakeTranslation(xOffset, yOffset);
|
} completion:nil];
|
}
|
|
- (void)bounceAnimation {
|
CGFloat xOffset = 0;
|
CGFloat yOffset = 0;
|
switch (self.direction) {
|
case AMPopTipDirectionUp:
|
yOffset = -self.actionBounceOffset;
|
break;
|
case AMPopTipDirectionDown:
|
yOffset = self.actionBounceOffset;
|
break;
|
case AMPopTipDirectionLeft:
|
xOffset = -self.actionBounceOffset;
|
break;
|
case AMPopTipDirectionRight:
|
xOffset = self.actionBounceOffset;
|
break;
|
case AMPopTipDirectionNone:
|
yOffset = -self.actionBounceOffset;
|
break;
|
}
|
|
[UIView animateWithDuration:(self.actionAnimationIn / 10) delay:self.actionDelayIn options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionAllowUserInteraction) animations:^{
|
self.transform = CGAffineTransformMakeTranslation(xOffset, yOffset);
|
} completion:^(BOOL finished) {
|
[UIView animateWithDuration:(self.actionAnimationIn - self.actionAnimationIn / 10) delay:0 usingSpringWithDamping:0.4 initialSpringVelocity:1 options:0 animations:^{
|
self.transform = CGAffineTransformIdentity;
|
} completion:^(BOOL done) {
|
if (self.shouldBounce && done) {
|
[self bounceAnimation];
|
}
|
}];
|
}];
|
}
|
|
- (void)pulseAnimation {
|
[UIView animateWithDuration:(self.actionAnimationIn / 2) delay:self.actionDelayIn options:(UIViewAnimationOptionRepeat | UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAutoreverse | UIViewAnimationOptionAllowUserInteraction) animations:^{
|
self.transform = CGAffineTransformMakeScale(self.actionPulseOffset, self.actionPulseOffset);
|
} completion:nil];
|
}
|
|
- (void)dismissActionAnimation {
|
self.shouldBounce = NO;
|
[UIView animateWithDuration:(self.actionAnimationOut / 2) delay:self.actionDelayOut options:UIViewAnimationOptionBeginFromCurrentState animations:^{
|
self.transform = CGAffineTransformIdentity;
|
} completion:^(BOOL finished) {
|
[self.layer removeAllAnimations];
|
}];
|
}
|
|
|
@end
|