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.
410 lines
12 KiB
410 lines
12 KiB
4 years ago
|
//
|
||
|
// 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
|