iOS开发-下拉刷新动画CAShapeLayer的strokeStart与strokeEnd指示器动画效果

iOS开发-下拉刷新动画CAShapeLayer的strokeStart与strokeEnd刷新指示器效果

之前开发中实现下拉刷新动画CAShapeLayer的strokeStart与strokeEnd指示器动画效果

一、效果图

在这里插入图片描述

二、基础动画

CABasicAnimation类的使用方式就是基本的关键帧动画。

所谓关键帧动画,就是将Layer的属性作为KeyPath来注册,指定动画的起始帧和结束帧,然后自动计算和实现中间的过渡动画的一种动画方式。

可以查看

https://blog.csdn.net/gloryFlow/article/details/131991202

三、实现代码

CAShapeLayer的strokeStart和strokeEnd属性
苹果官方给出这两个属性的解释为:

/* These values define the subregion of the path used to draw the
* stroked outline. The values must be in the range [0,1] with zero
* representing the start of the path and one the end. Values in
* between zero and one are interpolated linearly along the path
* length. strokeStart defaults to zero and strokeEnd to one. Both are
* animatable. */

我们可以对绘制的Path进行分区。这两个属性的值在0~1之间,0代表Path的开始位置,1代表Path的结束位置。是一种线性递增关系。strokeStart默认值为0,strokeEnd默认值为1。这两个属性都支持动画。

keyPath = strokeStart 动画的fromValue = 0,toValue = 1
表示从路径的0位置画到1 怎么画是按照清除开始的位置也就是清除0 一直清除到1 效果就是一条路径慢慢的消失

keyPath = strokeStart 动画的fromValue = 1,toValue = 0
表示从路径的1位置画到0 怎么画是按照清除开始的位置也就是1 这样开始的路径是空的(即都被清除掉了)一直清除到0 效果就是一条路径被反方向画出来

keyPath = strokeEnd 动画的fromValue = 0,toValue = 1
表示 这里我们分3个点说明动画的顺序 strokeEnd从结尾开始清除 首先整条路径先清除后2/3,接着清除1/3 效果就是正方向画出路径

keyPath = strokeEnd 动画的fromValue = 1,toValue = 0
效果就是反方向路径慢慢消失
注释: 动画的0-1(fromValue = 0,toValue = 1) 或1-0 (fromValue = 1,toValue = 0) 表示执行的方向 和路径的范围。

  • strokeStart 把一个圆先画完,然后 再慢慢减少
  • strokeEnd 从原点开始画,然后把圆画完整

这部分介绍参考https://www.jianshu.com/p/2f5d1b2f1261

我们主要实现动画效果。

3.1 代码实现动画

主要实现CABasicAnimation动画,KeyPath是strokeStart、strokeEnd

- (void)strokeAnimation {
    
    
    CABasicAnimation *strokeStartAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAnimation.fromValue         = @0.0;
    strokeStartAnimation.toValue           = @1.0;
    strokeStartAnimation.timingFunction    = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    CABasicAnimation *strokeEndAnimation   = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAnimation.fromValue           = @0.0;
    strokeEndAnimation.toValue             = @1.0;
    strokeEndAnimation.timingFunction      = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeEndAnimation.duration            = self.duration * 0.5;
    
    CAAnimationGroup *strokeAniamtionGroup = [CAAnimationGroup animation];
    strokeAniamtionGroup.duration          = self.duration;
    strokeAniamtionGroup.fillMode          = kCAFillModeForwards;
    strokeAniamtionGroup.removedOnCompletion = NO;
    
    strokeAniamtionGroup.delegate          = self;
    strokeAniamtionGroup.animations        = @[strokeEndAnimation,strokeStartAnimation];
    [self.loadingLayer addAnimation:strokeAniamtionGroup forKey:@"strokeAniamtion"];
}

完整代码如下

#import "INRefreshCircleLoading.h"
#import "UIColor+Addition.h"

static CGFloat kCircleSize = 30.0;

@interface INRefreshCircleLoading ()<CAAnimationDelegate>

@property (nonatomic, strong) CAShapeLayer *loadingLayer;

@property (nonatomic, assign) CGFloat duration;

@property (nonatomic, assign) CGFloat startAngle;
@property (nonatomic, assign) BOOL animationStoped;
@property (nonatomic, assign) BOOL isLoading;

@end

@implementation INRefreshCircleLoading

- (instancetype)initWithFrame:(CGRect)frame
{
    
    
    self = [super initWithFrame:frame];
    if (self) {
    
    
        
        self.duration = 1.0;
        self.isLoading = NO;
        self.animationStoped = NO;
        
        [self.layer addSublayer:self.loadingLayer];
        
        [self layoutSubLayersFrame];
        [self drawContentShapeLayer];
    }
    return self;
}

- (void)layoutSubLayersFrame {
    
    
    self.loadingLayer.frame = CGRectMake((CGRectGetWidth(self.bounds) - kCircleSize)/2,  (CGRectGetHeight(self.bounds) - kCircleSize)/2, kCircleSize, kCircleSize);
}

- (void)drawContentShapeLayer {
    
    
    // Drawing code
    [self drawCirclelayer];
}

- (void)displayPrecent:(CGFloat)precent {
    
    
    if (precent < 0) {
    
    
        precent = 0;
    }
    
    if (precent > 1.0) {
    
    
        precent = 1.0;
    }
    
    if (!self.isLoading) {
    
    
        self.loadingLayer.strokeStart = precent;
    }
}

- (void)startAnimation {
    
    
    self.animationStoped = NO;
    self.isLoading = YES;
    [self strokeAnimation];
}

- (void)stopAnimation {
    
    
    [self.loadingLayer removeAnimationForKey:@"strokeAniamtion"];
    self.animationStoped = YES;
    self.isLoading = NO;
    self.startAngle = 0.0;
}

- (void)strokeAnimation {
    
    
    CABasicAnimation *strokeStartAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    strokeStartAnimation.fromValue         = @0.0;
    strokeStartAnimation.toValue           = @1.0;
    strokeStartAnimation.timingFunction    = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    
    CABasicAnimation *strokeEndAnimation   = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    strokeEndAnimation.fromValue           = @0.0;
    strokeEndAnimation.toValue             = @1.0;
    strokeEndAnimation.timingFunction      = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    strokeEndAnimation.duration            = self.duration * 0.5;
    
    CAAnimationGroup *strokeAniamtionGroup = [CAAnimationGroup animation];
    strokeAniamtionGroup.duration          = self.duration;
    strokeAniamtionGroup.fillMode          = kCAFillModeForwards;
    strokeAniamtionGroup.removedOnCompletion = NO;
    
    strokeAniamtionGroup.delegate          = self;
    strokeAniamtionGroup.animations        = @[strokeEndAnimation,strokeStartAnimation];
    [self.loadingLayer addAnimation:strokeAniamtionGroup forKey:@"strokeAniamtion"];
}

#pragma mark - CAAnimationDelegate
- (void)animationDidStart:(CAAnimation *)anim {
    
    
    
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    
    
    CAAnimation *animate = [self.loadingLayer animationForKey:@"strokeAniamtion"];
    if (!(animate == anim)) {
    
    
        return;
    }
    
    if (flag) {
    
    
        if (self.animationStoped) {
    
    
            // 结束掉动画,否则出现循环
            return;
        }
        [self drawCirclelayer];
        [self strokeAnimation];
    }    
}

#pragma mark - DrawShapeLayer
- (void)drawCirclelayer {
    
    
    
    CGFloat endAngle = self.startAngle - (M_PI/180*45);
    if (endAngle < 0.0) {
    
    
        endAngle = self.startAngle + (M_PI/180*315);
    }
    
    UIBezierPath* path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(kCircleSize/2, kCircleSize/2) radius:kCircleSize/2 startAngle:self.startAngle endAngle:endAngle clockwise:YES];
    
    UIGraphicsBeginImageContext(self.loadingLayer.frame.size);
    [path stroke];
    UIGraphicsEndImageContext();
    
    self.loadingLayer.path = path.CGPath;
    
    self.startAngle = endAngle;
}

#pragma mark - SETTER/GETTER
- (CAShapeLayer *)loadingLayer {
    
    
    if (!_loadingLayer) {
    
    
        _loadingLayer = [CAShapeLayer layer];
        _loadingLayer.backgroundColor = [UIColor clearColor].CGColor;
        //设置线条的宽度和颜色
        _loadingLayer.lineWidth = 2.0f;
        _loadingLayer.strokeColor = [UIColor colorWithHexString:@"ff7e48" alpha:1.0].CGColor;
        _loadingLayer.fillColor = [UIColor clearColor].CGColor;
        _loadingLayer.lineCap     = kCALineCapRound;
    }
    return _loadingLayer;
}

@end

3.2 MJRefresh使用该动画

我这里继承MJRefreshStateHeader

需要根据刷新控件的状态来执行开启动画与结束动画操作

刷新控件的状态如下

typedef NS_ENUM(NSInteger, MJRefreshState) {
    
    
    // 普通闲置状态
    MJRefreshStateIdle = 1,
    // 松开就可以进行刷新的状态
    MJRefreshStatePulling,
    // 正在刷新中的状态
    MJRefreshStateRefreshing,
    // 即将刷新的状态
    MJRefreshStateWillRefresh,
    // 所有数据加载完毕,没有更多的数据了
    MJRefreshStateNoMoreData
};

INRefreshHeader.h

#import "MJRefresh.h"
#import "INRefreshFourBallLoading.h"

@interface INRefreshHeader : MJRefreshStateHeader

@property (nonatomic, assign) BOOL showInsetTop;

@property (nonatomic, strong) INRefreshCircleLoading *circleLoading;

@end

INRefreshHeader.m

@implementation INRefreshHeader

- (instancetype)initWithFrame:(CGRect)frame {
    
    
    if (self = [super initWithFrame:frame]) {
    
    
        self.lastUpdatedTimeLabel.hidden = YES;
        self.stateLabel.hidden = YES;
        
        [self addSubview:self.circleLoading];
    }
    return self;
}

- (INRefreshCircleLoading *)circleLoading {
    
    
    if (!_circleLoading) {
    
    
        _circleLoading = [[INRefreshCircleLoading alloc] initWithFrame:CGRectMake(0.0, 0.0, CGRectGetWidth([UIScreen mainScreen].bounds), self.bounds.size.height)];
    }
    return _circleLoading;
}


- (void)setState:(MJRefreshState)state {
    
    
    MJRefreshCheckState
    
    // 根据状态做事情
    if (state == MJRefreshStateIdle) {
    
    
        if (oldState == MJRefreshStateRefreshing) {
    
    
            self.circleLoading.alpha = 1.0;

            // 如果执行完动画发现不是idle状态,就直接返回,进入其他状态
            if (self.state != MJRefreshStateIdle) return;
            
            self.circleLoading.alpha = 1.0;

            [self.circleLoading stopAnimation];

        } else {
    
    
            [self.circleLoading stopAnimation];

        }
    } else if (state == MJRefreshStatePulling) {
    
    
        [self.circleLoading stopAnimation];

    } else if (state == MJRefreshStateRefreshing) {
    
    
        self.circleLoading.alpha = 1.0;
    }
}

- (void)prepare {
    
    
    [super prepare];
    self.mj_h = 60.0;
}

- (void)placeSubviews {
    
    
    [super placeSubviews];
    
    CGFloat centerX = self.mj_w * 0.5;
    CGFloat centerY = self.mj_h * 0.5;
    self.circleLoading.center = CGPointMake(centerX, centerY);
}

/** 当scrollView的contentOffset发生改变的时候调用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change {
    
    
    [super scrollViewContentOffsetDidChange:change];
    NSLog(@"change:%@",change);
    
    CGPoint old = [change[@"old"] CGPointValue];
    CGPoint new = [change[@"new"] CGPointValue];
    
    CGFloat precent = -new.y/self.mj_h;
    [self.circleLoading displayIndicator:precent];
}

/** 当scrollView的contentSize发生改变的时候调用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
    
    
    [super scrollViewContentSizeDidChange:change];
}

/** 当scrollView的拖拽状态发生改变的时候调用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change {
    
    
    [super scrollViewPanStateDidChange:change];
}

- (void)setShowInsetTop:(BOOL)showInsetTop {
    
    
    _showInsetTop = showInsetTop;
    
}

- (void)backInitState {
    
    
    
}

@end

3.3 具体的TableView使用

需要设置UITableView的下拉刷新操作:tableView.mj_header = header

- (void)configureRefresh {
    
    
    __weak typeof(self) weakSelf = self;
    INRefreshHeader *header = [INRefreshHeader headerWithRefreshingBlock:^{
    
    
        [weakSelf refreshData];
    }];
    
    INRefreshFooter *footer = [INRefreshFooter footerWithRefreshingBlock:^{
    
    
        [weakSelf loadMoreData];
    }];
    self.editView.tableView.mj_header = header;
    self.editView.tableView.mj_footer = footer;
}

- (void)refreshData {
    
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
        [self.editView.tableView.mj_header endRefreshing];
        [self.editView.tableView.mj_footer endRefreshing];
    });
}

- (void)loadMoreData {
    
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
        [self.editView.tableView.mj_header endRefreshing];
        [self.editView.tableView.mj_footer endRefreshing];
    });
}

四、小结

iOS开发-下拉刷新动画CAShapeLayer的strokeStart与strokeEnd刷新指示器效果,使用mjrefresh一个好用的上下拉刷新的控件。实现CABasicAnimation基础效果,根据不同的mjrefresh下拉刷新操作来执行动画效果。

学习记录,每天不停进步。

猜你喜欢

转载自blog.csdn.net/gloryFlow/article/details/131993291