You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
409 lines
12 KiB
409 lines
12 KiB
// |
|
// WXYZ_BadgeView.m |
|
// WXReader |
|
// |
|
// Created by Andrew on 2020/8/7. |
|
// Copyright © 2020 Andrew. All rights reserved. |
|
// |
|
|
|
#import "WXYZ_BadgeView.h" |
|
#import <QuartzCore/QuartzCore.h> |
|
|
|
//%%% default diameter |
|
CGFloat const RKNotificationHubDefaultDiameter = 30; |
|
static CGFloat const kCountMagnitudeAdaptationRatio = 0.3; |
|
//%%% pop values |
|
static CGFloat const kPopStartRatio = .85; |
|
static CGFloat const kPopOutRatio = 1.05; |
|
static CGFloat const kPopInRatio = .95; |
|
|
|
//%%% blink values |
|
static CGFloat const kBlinkDuration = 0.1; |
|
static CGFloat const kBlinkAlpha = 0.1; |
|
|
|
//%%% bump values |
|
static CGFloat const kFirstBumpDistance = 8.0; |
|
static CGFloat const kBumpTimeSeconds = 0.13; |
|
static CGFloat const SECOND_BUMP_DIST = 4.0; |
|
static CGFloat const kBumpTimeSeconds2 = 0.1; |
|
|
|
@interface RKView : UIView |
|
@property (nonatomic) BOOL isUserChangingBackgroundColor; |
|
@end |
|
|
|
@implementation RKView |
|
|
|
- (void)setBackgroundColor:(UIColor *)backgroundColor |
|
{ |
|
if (self.isUserChangingBackgroundColor) { |
|
super.backgroundColor = backgroundColor; |
|
self.isUserChangingBackgroundColor = NO; |
|
} |
|
} |
|
|
|
@end |
|
|
|
|
|
@implementation WXYZ_BadgeView{ |
|
int curOrderMagnitude; |
|
UILabel *countLabel; |
|
RKView *redCircle; |
|
CGPoint initialCenter; |
|
CGRect baseFrame; |
|
CGRect initialFrame; |
|
BOOL isIndeterminateMode; |
|
} |
|
|
|
@synthesize hubView; |
|
|
|
#pragma mark - SETUP |
|
|
|
- (id)initWithView:(UIView *)view |
|
{ |
|
self = [super init]; |
|
if (!self) return nil; |
|
|
|
_maxCount = 100000; |
|
[self setView:view andCount:0]; |
|
|
|
return self; |
|
} |
|
|
|
- (id)initWithBarButtonItem:(UIBarButtonItem *)barButtonItem |
|
{ |
|
self = [self initWithView:[barButtonItem valueForKey:@"view"]]; |
|
[self scaleCircleSizeBy:0.7]; |
|
[self moveCircleByX:-5.0 Y:0]; |
|
|
|
return self; |
|
} |
|
|
|
//%%% give this a view and an initial count (0 hides the notification circle) |
|
// and it will make a hub for you |
|
- (void)setView:(UIView *)view andCount:(int)startCount |
|
{ |
|
curOrderMagnitude = 0; |
|
|
|
CGRect frame = view.frame; |
|
|
|
isIndeterminateMode = NO; |
|
|
|
redCircle = [[RKView alloc]init]; |
|
redCircle.userInteractionEnabled = NO; |
|
redCircle.isUserChangingBackgroundColor = YES; |
|
redCircle.backgroundColor = [UIColor redColor]; |
|
|
|
countLabel = [[UILabel alloc]initWithFrame:redCircle.frame]; |
|
countLabel.userInteractionEnabled = NO; |
|
self.count = startCount; |
|
[countLabel setTextAlignment:NSTextAlignmentCenter]; |
|
countLabel.textColor = [UIColor whiteColor]; |
|
countLabel.backgroundColor = [UIColor clearColor]; |
|
|
|
[self setCircleAtFrame:CGRectMake(frame.size.width- (RKNotificationHubDefaultDiameter*2/3), -RKNotificationHubDefaultDiameter/3, RKNotificationHubDefaultDiameter, RKNotificationHubDefaultDiameter)]; |
|
|
|
[view addSubview:redCircle]; |
|
[view addSubview:countLabel]; |
|
[view bringSubviewToFront:redCircle]; |
|
[view bringSubviewToFront:countLabel]; |
|
hubView = view; |
|
[self checkZero]; |
|
} |
|
|
|
//%%% set the frame of the notification circle relative to the button |
|
- (void)setCircleAtFrame:(CGRect)frame |
|
{ |
|
[redCircle setFrame:frame]; |
|
initialCenter = CGPointMake(frame.origin.x+frame.size.width/2, frame.origin.y+frame.size.height/2); |
|
baseFrame = frame; |
|
initialFrame = frame; |
|
countLabel.frame = redCircle.frame; |
|
redCircle.layer.cornerRadius = frame.size.height/2; |
|
[countLabel setFont:[UIFont fontWithName:@"HelveticaNeue" size:frame.size.width/2]]; |
|
[self expandToFitLargerDigits]; |
|
} |
|
|
|
//%%% moves the circle by x amount on the x axis and y amount on the y axis |
|
- (void)moveCircleByX:(CGFloat)x Y:(CGFloat)y |
|
{ |
|
CGRect frame = redCircle.frame; |
|
frame.origin.x += x; |
|
frame.origin.y += y; |
|
[self setCircleAtFrame:frame]; |
|
} |
|
|
|
//%%% changes the size of the circle. setting a scale of 1 has no effect |
|
- (void)scaleCircleSizeBy:(CGFloat)scale |
|
{ |
|
CGRect fr = initialFrame; |
|
CGFloat width = fr.size.width * scale; |
|
CGFloat height = fr.size.height * scale; |
|
CGFloat wdiff = (fr.size.width - width) / 2; |
|
CGFloat hdiff = (fr.size.height - height) / 2; |
|
|
|
CGRect frame = CGRectMake(fr.origin.x + wdiff, fr.origin.y + hdiff, width, height); |
|
[self setCircleAtFrame:frame]; |
|
} |
|
|
|
//%%% change the color of the notification circle |
|
- (void)setCircleColor:(UIColor*)circleColor labelColor:(UIColor*)labelColor |
|
{ |
|
redCircle.isUserChangingBackgroundColor = YES; |
|
redCircle.backgroundColor = circleColor; |
|
[countLabel setTextColor:labelColor]; |
|
} |
|
|
|
- (void)setCircleBorderColor:(UIColor *)color borderWidth:(CGFloat)width { |
|
redCircle.layer.borderColor = color.CGColor; |
|
redCircle.layer.borderWidth = width; |
|
} |
|
|
|
- (void)hideCount |
|
{ |
|
countLabel.hidden = YES; |
|
isIndeterminateMode = YES; |
|
} |
|
|
|
- (void)showCount |
|
{ |
|
isIndeterminateMode = NO; |
|
[self checkZero]; |
|
} |
|
|
|
#pragma mark - ATTRIBUTES |
|
|
|
//%%% increases count by 1 |
|
- (void)increment |
|
{ |
|
[self incrementBy:1]; |
|
} |
|
|
|
//%%% increases count by amount |
|
- (void)incrementBy:(int)amount |
|
{ |
|
self.count += amount; |
|
} |
|
|
|
//%%% decreases count |
|
- (void)decrement |
|
{ |
|
[self decrementBy:1]; |
|
} |
|
|
|
//%%% decreases count by amount |
|
- (void)decrementBy:(int)amount |
|
{ |
|
if (amount >= self.count) { |
|
self.count = 0; |
|
return; |
|
} |
|
self.count -= amount; |
|
} |
|
|
|
//%%% set the count yourself |
|
- (void)setCount:(int)newCount |
|
{ |
|
_count = newCount; |
|
|
|
NSString *labelText = [NSString stringWithFormat:@"%@", @(self.count)]; |
|
|
|
if (_count > self.maxCount){ |
|
labelText = [NSString stringWithFormat:@"%@+", @(self.maxCount)]; |
|
} |
|
|
|
countLabel.text = labelText; |
|
[self checkZero]; |
|
[self expandToFitLargerDigits]; |
|
} |
|
|
|
//%% set the font of the label |
|
- (void)setCountLabelFont:(UIFont *)font |
|
{ |
|
[countLabel setFont:font]; |
|
} |
|
|
|
- (UIFont *)countLabelFont |
|
{ |
|
return countLabel.font; |
|
} |
|
|
|
#pragma mark - ANIMATION |
|
|
|
//%%% animation that resembles facebook's pop |
|
- (void)pop |
|
{ |
|
const float height = baseFrame.size.height; |
|
const float width = baseFrame.size.width; |
|
const float pop_start_h = height * kPopStartRatio; |
|
const float pop_start_w = width * kPopStartRatio; |
|
const float time_start = 0.05; |
|
const float pop_out_h = height * kPopOutRatio; |
|
const float pop_out_w = width * kPopOutRatio; |
|
const float time_out = .2; |
|
const float pop_in_h = height * kPopInRatio; |
|
const float pop_in_w = width * kPopInRatio; |
|
const float time_in = .05; |
|
const float pop_end_h = height; |
|
const float pop_end_w = width; |
|
const float time_end = 0.05; |
|
|
|
CABasicAnimation *startSize = [CABasicAnimation animationWithKeyPath:@"cornerRadius"]; |
|
startSize.duration = time_start; |
|
startSize.beginTime = 0; |
|
startSize.fromValue = @(pop_end_h / 2); |
|
startSize.toValue = @(pop_start_h / 2); |
|
startSize.removedOnCompletion = FALSE; |
|
|
|
CABasicAnimation *outSize = [CABasicAnimation animationWithKeyPath:@"cornerRadius"]; |
|
outSize.duration = time_out; |
|
outSize.beginTime = time_start; |
|
outSize.fromValue = startSize.toValue; |
|
outSize.toValue = @(pop_out_h / 2); |
|
outSize.removedOnCompletion = FALSE; |
|
|
|
CABasicAnimation *inSize = [CABasicAnimation animationWithKeyPath:@"cornerRadius"]; |
|
inSize.duration = time_in; |
|
inSize.beginTime = time_start+time_out; |
|
inSize.fromValue = outSize.toValue; |
|
inSize.toValue = @(pop_in_h / 2); |
|
inSize.removedOnCompletion = FALSE; |
|
|
|
CABasicAnimation *endSize = [CABasicAnimation animationWithKeyPath:@"cornerRadius"]; |
|
endSize.duration = time_end; |
|
endSize.beginTime = time_in+time_out+time_start; |
|
endSize.fromValue = inSize.toValue; |
|
endSize.toValue = @(pop_end_h / 2); |
|
endSize.removedOnCompletion = FALSE; |
|
|
|
CAAnimationGroup *group = [CAAnimationGroup animation]; |
|
[group setDuration: time_start+time_out+time_in+time_end]; |
|
[group setAnimations:@[startSize, outSize, inSize, endSize]]; |
|
|
|
[redCircle.layer addAnimation:group forKey:nil]; |
|
|
|
[UIView animateWithDuration:time_start animations:^{ |
|
CGRect frame = redCircle.frame; |
|
CGPoint center = redCircle.center; |
|
frame.size.height = pop_start_h; |
|
frame.size.width = pop_start_w; |
|
redCircle.frame = frame; |
|
redCircle.center = center; |
|
}completion:^(BOOL complete){ |
|
[UIView animateWithDuration:time_out animations:^{ |
|
CGRect frame = redCircle.frame; |
|
CGPoint center = redCircle.center; |
|
frame.size.height = pop_out_h; |
|
frame.size.width = pop_out_w; |
|
redCircle.frame = frame; |
|
redCircle.center = center; |
|
}completion:^(BOOL complete){ |
|
[UIView animateWithDuration:time_in animations:^{ |
|
CGRect frame = redCircle.frame; |
|
CGPoint center = redCircle.center; |
|
frame.size.height = pop_in_h; |
|
frame.size.width = pop_in_w; |
|
redCircle.frame = frame; |
|
redCircle.center = center; |
|
}completion:^(BOOL complete){ |
|
[UIView animateWithDuration:time_end animations:^{ |
|
CGRect frame = redCircle.frame; |
|
CGPoint center = redCircle.center; |
|
frame.size.height = pop_end_h; |
|
frame.size.width = pop_end_w; |
|
redCircle.frame = frame; |
|
redCircle.center = center; |
|
}]; |
|
}]; |
|
}]; |
|
}]; |
|
} |
|
|
|
//%%% animation that flashes on an off |
|
- (void)blink |
|
{ |
|
[self setAlpha:kBlinkAlpha]; |
|
|
|
[UIView animateWithDuration:kBlinkDuration animations:^{ |
|
[self setAlpha:1]; |
|
}completion:^(BOOL complete){ |
|
[UIView animateWithDuration:kBlinkDuration animations:^{ |
|
[self setAlpha:kBlinkAlpha]; |
|
}completion:^(BOOL complete){ |
|
[UIView animateWithDuration:kBlinkDuration animations:^{ |
|
[self setAlpha:1]; |
|
}]; |
|
}]; |
|
}]; |
|
} |
|
|
|
//%%% animation that jumps similar to OSX dock icons |
|
- (void)bump |
|
{ |
|
if (!CGPointEqualToPoint(initialCenter,redCircle.center)) { |
|
//%%% canel previous animation |
|
} |
|
|
|
[self bumpCenterY:0]; |
|
[UIView animateWithDuration:kBumpTimeSeconds animations:^{ |
|
[self bumpCenterY:kFirstBumpDistance]; |
|
}completion:^(BOOL complete){ |
|
[UIView animateWithDuration:kBumpTimeSeconds animations:^{ |
|
[self bumpCenterY:0]; |
|
}completion:^(BOOL complete){ |
|
[UIView animateWithDuration:kBumpTimeSeconds2 animations:^{ |
|
[self bumpCenterY:SECOND_BUMP_DIST]; |
|
}completion:^(BOOL complete){ |
|
[UIView animateWithDuration:kBumpTimeSeconds2 animations:^{ |
|
[self bumpCenterY:0]; |
|
}]; |
|
}]; |
|
}]; |
|
}]; |
|
} |
|
|
|
#pragma mark - HELPERS |
|
|
|
//%%% changes the Y origin of the notification circle |
|
- (void)bumpCenterY:(float)yVal |
|
{ |
|
CGPoint center = redCircle.center; |
|
center.y = initialCenter.y-yVal; |
|
redCircle.center = center; |
|
countLabel.center = center; |
|
} |
|
|
|
- (void)setAlpha:(float)alpha |
|
{ |
|
redCircle.alpha = alpha; |
|
countLabel.alpha = alpha; |
|
} |
|
|
|
//%%% hides the notification if the value is 0 |
|
- (void)checkZero |
|
{ |
|
if (self.count <= 0) { |
|
redCircle.hidden = YES; |
|
countLabel.hidden = YES; |
|
} else { |
|
redCircle.hidden = NO; |
|
if (!isIndeterminateMode) { |
|
countLabel.hidden = NO; |
|
} |
|
} |
|
} |
|
|
|
- (void)expandToFitLargerDigits { |
|
int orderOfMagnitude = log10((double)self.count); |
|
orderOfMagnitude = (orderOfMagnitude >= 2) ? orderOfMagnitude : 1; |
|
CGRect frame = initialFrame; |
|
frame.size.width = initialFrame.size.width * (1 + kCountMagnitudeAdaptationRatio * (orderOfMagnitude - 1)); |
|
frame.origin.x = initialFrame.origin.x - (frame.size.width - initialFrame.size.width) / 2; |
|
|
|
[redCircle setFrame:frame]; |
|
initialCenter = CGPointMake(frame.origin.x+frame.size.width/2, frame.origin.y+frame.size.height/2); |
|
baseFrame = frame; |
|
countLabel.frame = redCircle.frame; |
|
curOrderMagnitude = orderOfMagnitude; |
|
} |
|
|
|
@end
|
|
|