iOS UIButton倒计时、指示器、粒子效果

语言: CN / TW / HK

前言

  • 分享三款按钮来使用, 倒计时按钮,指示器按钮,点赞粒子效果按钮

倒计时按钮

Property & API

@interface UIButton (KJCountDown)
/// 倒计时结束的回调
@property(nonatomic,copy,readwrite)void(^kButtonCountDownStop)(void);
/// 设置倒计时的间隔和倒计时文案,默认为 @"%zd秒"
- (void)kj_startTime:(NSInteger)timeout CountDownFormat:(NSString*)format;
/// 取消倒计时
- (void)kj_cancelTimer;

@end
复制代码

简单介绍

正在倒计时的按钮是不可点击,内部主要就是声明一个计时器NSTimer来处理倒计时按钮,在计时期间关闭userInteractionEnabled属性

1. kButtonCountDownStop

倒计时结束时刻调用该回调

2. kj_startTime:CountDownFormat:

开始计时

- (void)kj_startTime:(NSInteger)timeout CountDownFormat:(NSString*)format{
    [self kj_cancelTimer];
    self.timeOut = timeout;
    self.xxtitle = self.titleLabel.text;
    NSDictionary *info = @{@"countDownFormat":format ?: @"%zd秒"};
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod:) userInfo:info repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    dispatch_async(dispatch_get_main_queue(), ^{
        [self setTitle:[NSString stringWithFormat:format ?: @"%zd秒",timeout] forState:UIControlStateNormal];
        self.userInteractionEnabled = NO;
    });
}
- (void)timerMethod:(NSTimer*)timer{
    NSDictionary *info = timer.userInfo;
    NSString *countDownFormat = info[@"countDownFormat"];
    if (self.timeOut <= 0){
        [self kj_cancelTimer];
    }else{
        self.timeOut--;
        dispatch_async(dispatch_get_main_queue(), ^{
            [self setTitle:[NSString stringWithFormat:countDownFormat,self.timeOut] forState:UIControlStateNormal];
            self.userInteractionEnabled = NO;
        });
    }
}
复制代码

3. kj_cancelTimer

取消倒计时

- (void)kj_cancelTimer{
    if (self.timer == nil) return;
    [self.timer invalidate];
    self.timer = nil;
    dispatch_async(dispatch_get_main_queue(), ^{
        [self setTitle:self.xxtitle forState:UIControlStateNormal];
        self.userInteractionEnabled = YES;
        if (self.kButtonCountDownStop) { self.kButtonCountDownStop(); }
    });
}
复制代码

内部Property

- (NSTimer*)timer{
    return objc_getAssociatedObject(self, @selector(timer));
}
- (void)setTimer:(NSTimer*)timer{
    objc_setAssociatedObject(self, @selector(timer), timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString*)xxtitle{
    return objc_getAssociatedObject(self, @selector(xxtitle));
}
- (void)setXxtitle:(NSString*)xxtitle{
    objc_setAssociatedObject(self, @selector(xxtitle), xxtitle, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)setKButtonCountDownStop:(void(^)(void))kButtonCountDownStop{
    objc_setAssociatedObject(self, @selector(kButtonCountDownStop), kButtonCountDownStop, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void(^)(void))kButtonCountDownStop{
    return objc_getAssociatedObject(self, @selector(kButtonCountDownStop));
}
- (NSInteger)timeOut{
    return [objc_getAssociatedObject(self, @selector(timeOut)) integerValue];
}
- (void)setTimeOut:(NSInteger)timeOut{
    objc_setAssociatedObject(self, @selector(timeOut), @(timeOut), OBJC_ASSOCIATION_ASSIGN);
}
复制代码

使用示例

[_countDownButton kj_addAction:^(UIButton * _Nonnull kButton) {
    [kButton kj_startTime:6 CountDownFormat:@"计时%zd秒"];
}];
_countDownButton.kButtonCountDownStop = ^{
    NSLog(@"计时结束!!!");
};
复制代码

指示器按钮

Property & API

@interface UIButton (KJIndicator)
/// 按钮是否正在提交中
@property(nonatomic,assign,readonly)bool submitting;
/// 指示器和文字间隔,默认5px
@property(nonatomic,assign)CGFloat indicatorSpace;
/// 指示器颜色,默认白色
@property(nonatomic,assign)UIActivityIndicatorViewStyle indicatorType;

/// 开始提交,指示器跟随文字
- (void)kj_beginSubmitting:(NSString*)title;
/// 结束提交
- (void)kj_endSubmitting;
/// 显示指示器
- (void)kj_showIndicator;
/// 隐藏指示器
- (void)kj_hideIndicator;

@end
复制代码

简单介绍

其实就是在按钮内部放文本UILabel和指示器UIActivityIndicatorView

内部出了

#import "UIButton+KJIndicator.h"
#import <objc/runtime.h>

@implementation UIButton (KJIndicator)
static NSString *kIndicatorLastTitle = nil;
- (void)kj_beginSubmitting:(NSString*)title{
    [self kj_endSubmitting];
    kSubmitting = true;
    kIndicatorLastTitle = self.titleLabel.text;
    self.enabled = NO;
    [self setTitle:@"" forState:UIControlStateNormal];
    
    self.indicatorType = self.indicatorType?:UIActivityIndicatorViewStyleWhite;
    self.indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:self.indicatorType];
    [self addSubview:self.indicatorView];
    
    self.indicatorSpace = self.indicatorSpace?:5;
    CGFloat w = self.bounds.size.width;
    CGFloat h = self.bounds.size.height;
    CGFloat sp = w / 2.;
    if (![title isEqualToString:@""]) {
        self.indicatorLabel = [[UILabel alloc] init];
        self.indicatorLabel.text = title;
        self.indicatorLabel.font = self.titleLabel.font;
        self.indicatorLabel.textColor = self.titleLabel.textColor;
        [self addSubview:self.indicatorLabel];
        
        CGSize size = [title boundingRectWithSize:CGSizeMake(MAXFLOAT,0.0) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:self.titleLabel.font} context:nil].size;
        sp = ((w-self.indicatorSpace-size.width)*.5)?:0.0;
        self.indicatorLabel.frame = CGRectMake(sp+self.indicatorSpace+self.indicatorView.frame.size.width/2, 0, size.width, h);
    }
    
    self.indicatorView.center = CGPointMake(sp, h/2);
    [self.indicatorView startAnimating];
}

- (void)kj_endSubmitting {
    [self kj_hideIndicator];
    self.indicatorView = nil;
    self.indicatorLabel = nil;
}

- (void)kj_showIndicator {
    if (self.indicatorView && self.indicatorView.superview == nil) {
        [self addSubview:self.indicatorView];
        [self.indicatorView startAnimating];
    }
    if (self.indicatorLabel && self.indicatorLabel.superview == nil) {
        [self addSubview:self.indicatorLabel];
        [self setTitle:@"" forState:UIControlStateNormal];
    }
}

- (void)kj_hideIndicator {
    kSubmitting = false;
    self.enabled = YES;
    
    [self.indicatorView removeFromSuperview];
    [self.indicatorLabel removeFromSuperview];
    
    if (self.indicatorLabel) {
        [self setTitle:kIndicatorLastTitle forState:UIControlStateNormal];
    }
    if (self.indicatorView) {
        [self.indicatorView stopAnimating];
        [self setTitle:kIndicatorLastTitle forState:UIControlStateNormal];
    }
}

#pragma mark - getter/setter
static bool kSubmitting = false;
- (bool)submitting{
    return kSubmitting;
}
- (CGFloat)indicatorSpace{
    return [objc_getAssociatedObject(self, @selector(indicatorSpace)) floatValue];
}
- (void)setIndicatorSpace:(CGFloat)indicatorSpace{
    objc_setAssociatedObject(self, @selector(indicatorSpace), @(indicatorSpace), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIActivityIndicatorViewStyle)indicatorType{
    return (UIActivityIndicatorViewStyle)[objc_getAssociatedObject(self, @selector(indicatorType)) intValue];
}
- (void)setIndicatorType:(UIActivityIndicatorViewStyle)indicatorType{
    objc_setAssociatedObject(self, @selector(indicatorType), @(indicatorType), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIActivityIndicatorView*)indicatorView{
    return objc_getAssociatedObject(self, @selector(indicatorView));
}
- (void)setIndicatorView:(UIActivityIndicatorView*)indicatorView{
    objc_setAssociatedObject(self, @selector(indicatorView), indicatorView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UILabel*)indicatorLabel{
    return objc_getAssociatedObject(self, @selector(indicatorLabel));
}
- (void)setIndicatorLabel:(UILabel*)indicatorLabel{
    objc_setAssociatedObject(self, @selector(indicatorLabel), indicatorLabel, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
复制代码

使用示例

/// 开启指示器
[button kj_addAction:^(UIButton * _Nonnull kButton) {
    [kButton kj_beginSubmitting:@"测试ing"];
}];

[button kj_addAction:^(UIButton * _Nonnull kButton) {
    kButton.selected = !kButton.selected;
    if (kButton.selected) {
        [weakself.submitButton kj_hideIndicator];
    }else{
        [weakself.submitButton kj_showIndicator];
    }
}];
复制代码

粒子效果

Property & API

@interface UIButton (KJEmitter)
/// 粒子,备注 name 属性不要更改
@property(nonatomic,strong,readonly)CAEmitterCell *emitterCell;
/// 设置粒子效果
- (void)kj_buttonSetEmitterImage:(UIImage*_Nullable)image OpenEmitter:(bool)open;

@end
复制代码

简单介绍

其实就是在setSelected之后去处理粒子效果

kj_buttonSetEmitterImage:

初始化效果,获取粒子图,设置CAEmitterLayer

- (void)kj_buttonSetEmitterImage:(UIImage*_Nullable)image OpenEmitter:(bool)open{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        method_exchangeImplementations(class_getInstanceMethod(self.class, @selector(setSelected:)), class_getInstanceMethod(self.class, @selector(kj_setSelected:)));
    });
    self.emitterImage = image?:[UIImage imageNamed:@"KJKit.bundle/button_sparkle"];
    self.emitterOpen = open;
    [self setupLayer];
}
复制代码

设置粒子效果的相关参数,考虑到自定义emitterImage的情况,所以还是把粒子emitterCell开放出去,这样也方便外界修改对应的参数(备注:name属性不要修改)

- (void)setupLayer{
    CAEmitterCell *emitterCell = [CAEmitterCell emitterCell];
    emitterCell.name = @"name";
    emitterCell.alphaRange = 0.10;
    emitterCell.lifetime = 0.7;
    emitterCell.lifetimeRange = 0.3;
    emitterCell.velocity = 40.00;
    emitterCell.velocityRange = 10.00;
    emitterCell.scale = 0.04;
    emitterCell.scaleRange = 0.02;
    emitterCell.contents = (id)self.emitterImage.CGImage;
    self.emitterCell = emitterCell;
    
    CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
    emitterLayer.name = @"emitterLayer";
    emitterLayer.emitterShape = kCAEmitterLayerCircle;
    emitterLayer.emitterMode = kCAEmitterLayerOutline;
    emitterLayer.emitterSize = CGSizeMake(10, 0);
    emitterLayer.emitterCells = @[emitterCell];
    emitterLayer.renderMode = kCAEmitterLayerOldestFirst;
    emitterLayer.position = CGPointMake(self.frame.size.width/2.0, self.frame.size.height/2.0);
    emitterLayer.zPosition = -1;
    [self.layer addSublayer:emitterLayer];
    self.explosionLayer = emitterLayer;
}
复制代码

开启粒子喷射和缩放效果

- (void)buttonAnimation{
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
    if (self.selected) {
        animation.values = @[@1.5 ,@0.8, @1.0,@1.2,@1.0];
        animation.duration = 0.4;
        /// 开始喷射
        self.explosionLayer.beginTime = CACurrentMediaTime();
        [self.explosionLayer setValue:@2000 forKeyPath:@"emitterCells.name.birthRate"];
        [self performSelector:@selector(stop) withObject:nil afterDelay:0.2];
    }else{
        animation.values = @[@0.8, @1.0];
        animation.duration = 0.2;
    }
    animation.calculationMode = kCAAnimationCubic;
    [self.layer addAnimation:animation forKey:@"transform.scale"];
}
复制代码

0.2秒之后停止喷射

[self performSelector:@selector(stop) withObject:nil afterDelay:0.2];
复制代码

停止喷射,其实就是将粒子的生命周期设置为零

- (void)stop {
    [self.explosionLayer setValue:@0 forKeyPath:@"emitterCells.name.birthRate"];
}
复制代码

附上完整代码

#import "UIButton+KJEmitter.h"
#import <objc/runtime.h>

@implementation UIButton (KJEmitter)
/// 设置粒子效果
- (void)kj_buttonSetEmitterImage:(UIImage*_Nullable)image OpenEmitter:(bool)open{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        method_exchangeImplementations(class_getInstanceMethod(self.class, @selector(setSelected:)), class_getInstanceMethod(self.class, @selector(kj_setSelected:)));
    });
    self.emitterImage = image?:[UIImage imageNamed:@"KJKit.bundle/button_sparkle"];
    self.emitterOpen = open;
    [self setupLayer];
}
/// 方法交换
- (void)kj_setSelected:(BOOL)selected{
    [self kj_setSelected:selected];
    if (self.emitterOpen) [self buttonAnimation];
}

- (UIImage*)emitterImage{
    return objc_getAssociatedObject(self, @selector(emitterImage));
}
- (void)setEmitterImage:(UIImage*)emitterImage{
    objc_setAssociatedObject(self, @selector(emitterImage), emitterImage, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)emitterOpen{
    return [objc_getAssociatedObject(self, @selector(emitterOpen)) intValue];
}
- (void)setEmitterOpen:(BOOL)emitterOpen{
    objc_setAssociatedObject(self, @selector(emitterOpen), @(emitterOpen), OBJC_ASSOCIATION_ASSIGN);
}
- (CAEmitterLayer*)explosionLayer{
    return objc_getAssociatedObject(self, @selector(explosionLayer));
}
- (void)setExplosionLayer:(CAEmitterLayer *)explosionLayer{
    objc_setAssociatedObject(self, @selector(explosionLayer), explosionLayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CAEmitterCell*)emitterCell{
    return objc_getAssociatedObject(self, @selector(emitterCell));
}
- (void)setEmitterCell:(CAEmitterCell*)emitterCell{
    objc_setAssociatedObject(self, @selector(emitterCell), emitterCell, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark - 粒子效果相关
- (void)setupLayer{
    CAEmitterCell *emitterCell = [CAEmitterCell emitterCell];
    emitterCell.name = @"name";
    emitterCell.alphaRange = 0.10;
    emitterCell.lifetime = 0.7;
    emitterCell.lifetimeRange = 0.3;
    emitterCell.velocity = 40.00;
    emitterCell.velocityRange = 10.00;
    emitterCell.scale = 0.04;
    emitterCell.scaleRange = 0.02;
    emitterCell.contents = (id)self.emitterImage.CGImage;
    self.emitterCell = emitterCell;
    
    CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
    emitterLayer.name = @"emitterLayer";
    emitterLayer.emitterShape = kCAEmitterLayerCircle;
    emitterLayer.emitterMode = kCAEmitterLayerOutline;
    emitterLayer.emitterSize = CGSizeMake(10, 0);
    emitterLayer.emitterCells = @[emitterCell];
    emitterLayer.renderMode = kCAEmitterLayerOldestFirst;
    emitterLayer.position = CGPointMake(self.frame.size.width/2.0, self.frame.size.height/2.0);
    emitterLayer.zPosition = -1;
    [self.layer addSublayer:emitterLayer];
    self.explosionLayer = emitterLayer;
}
/// 开始动画
- (void)buttonAnimation{
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
    if (self.selected) {
        animation.values = @[@1.5 ,@0.8, @1.0,@1.2,@1.0];
        animation.duration = 0.4;
        self.explosionLayer.beginTime = CACurrentMediaTime();
        [self.explosionLayer setValue:@2000 forKeyPath:@"emitterCells.name.birthRate"];
        [self performSelector:@selector(stop) withObject:nil afterDelay:0.2];
    }else{
        animation.values = @[@0.8, @1.0];
        animation.duration = 0.2;
    }
    animation.calculationMode = kCAAnimationCubic;
    [self.layer addAnimation:animation forKey:@"transform.scale"];
}
/// 停止喷射
- (void)stop {
    [self.explosionLayer setValue:@0 forKeyPath:@"emitterCells.name.birthRate"];
}

@end
复制代码

到此三种按钮就介绍完毕,有空再补充完善,码字累死我了- -|

备注:本文用到的部分函数方法和Demo,均来自三方库**KJExtensionHandler**,如有需要的朋友可自行pod 'KJExtensionHandler'引入即可

倒计时、指示器、粒子效果介绍就到此完毕,后面有相关再补充,写文章不容易,还请点个**小星星**传送门