模仿抖音加载动画效果

引言:

模仿『抖音』加载动画效果。


# 一. 效果图


# 二. 实现代码

自定义个 LCDYLodingView 继承自 UIView。

# LCDYLodingView.h


#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface LCDYLodingView : UIView


/**
 显示 loding
 */
+ (void)showLodingInView:(UIView *)view;


/**
 隐藏 loding
 */
+ (void)hideLodingInView:(UIView *)view;

@end

NS_ASSUME_NONNULL_END


# LCDYLodingView.m

#import "LCDYLodingView.h"

static CGFloat BallWidth     = 12.0f;       // 球宽
static CGFloat BallSpeed     = 0.7f;        // 球速
static CGFloat BallZoomScale = 0.25;        // 球缩放比例
static CGFloat PauseSecond   = 0.15;        // 暂停时间 s


@interface LCDYLodingView ()

@property (nonatomic, strong) UIView        *bgV;          // 背景视图
@property (nonatomic, strong) UIView        *redBallV;     // 红色圆球
@property (nonatomic, strong) UIView        *greenBallV;   // 绿色圆球
@property (nonatomic, strong) UIView        *blackBallV;   // 黑色圆球
@property (nonatomic, strong) CADisplayLink *displayLink;  // 定时器
@property (nonatomic, assign) BOOL          isDirection;   // 正向运动
@end

@implementation LCDYLodingView

#pragma mark - ------ Pubilc Methods ------

/// 显示
+ (void)showLodingInView:(UIView *)view {
    
    LCDYLodingView *loding = [[LCDYLodingView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
    loding.center = CGPointMake(SCREEN_WIDTH/2, SCREEN_HEIGHT/2 - 64);

    [view addSubview:loding];
}

/// 隐藏
+ (void)hideLodingInView:(UIView *)view {
    
    NSEnumerator *subViews = [view.subviews reverseObjectEnumerator];
    
    for (UIView *view in subViews) {
        
        if ([view isKindOfClass:self]) {
            LCDYLodingView *loding = (LCDYLodingView *)view;
            [loding.displayLink invalidate];
            [loding removeFromSuperview];
            return;
        }
    }
}

#pragma mark - ------ Lazyloding Methods ------

- (UIView *)bgV {
    
    if(!_bgV) {
        
        _bgV = [[UIView alloc] initWithFrame:(CGRectMake(0, 0, BallWidth*2, BallWidth*2))];
        _bgV.center = self.center;
    }
    return _bgV;
}

- (UIView *)redBallV {
    
    if(!_redBallV) {
        
        _redBallV = [[UIView alloc] init];
        _redBallV.lc_size = CGSizeMake(BallWidth, BallWidth);
        _redBallV.center = CGPointMake(_bgV.lc_width - BallWidth/2, _bgV.lc_height/2);
        _redBallV.layer.cornerRadius = BallWidth/2;
        _redBallV.layer.masksToBounds = YES;
        _redBallV.backgroundColor = [UIColor colorWithRed:255/255.0f green:46/255.0f blue:86/255.0f alpha:1];
    }
    return _redBallV;
}

- (UIView *)blackBallV {
    
    if(!_blackBallV) {
        
        _blackBallV = [[UIView alloc] init];
        _blackBallV.lc_size = CGSizeMake(BallWidth, BallWidth);
        _blackBallV.center = CGPointMake(BallWidth/2, _bgV.lc_height/2);
        _blackBallV.layer.cornerRadius = BallWidth/2;
        _blackBallV.layer.masksToBounds = YES;
        _blackBallV.backgroundColor = [UIColor colorWithRed:12/255.0f green:11/255.0f blue:17/255.0f alpha:1];
    }
    return _blackBallV;
}

- (UIView *)greenBallV {
    
    if(!_greenBallV) {
        
        _greenBallV = [[UIView alloc] init];
        _greenBallV.lc_size = CGSizeMake(BallWidth, BallWidth);
        _greenBallV.center = CGPointMake(BallWidth/2, _bgV.lc_height/2);
        _greenBallV.layer.cornerRadius = BallWidth/2;
        _greenBallV.layer.masksToBounds = YES;
        _greenBallV.backgroundColor = [UIColor colorWithRed:35/255.0f green:246/255.0f blue:235/255.0f alpha:1];
    }
    return _greenBallV;
}


#pragma mark - ------ System Methods ------

- (instancetype)initWithFrame:(CGRect)frame {
  
    self = [super initWithFrame:frame];
    
    if (self) {
        [self setupUILayout];
    }
    
    return self;
}

#pragma mark - ------ UILayout Methods ------
- (void)setupUILayout {
    
    self.backgroundColor = [UIColor clearColor];
    
    // 第一次动画,绿球在上、红球在下、黑色阴影在绿球上
    [self addSubview:self.bgV];
    [self.bgV addSubview:self.greenBallV];
    [self.bgV addSubview:self.redBallV];
    [self.greenBallV addSubview:self.blackBallV];
    self.isDirection = YES;
    
    // 定时器
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateBallAnimation)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}


/// 更新动画
- (void)updateBallAnimation {
    
    if (self.isDirection) {
        [self setupPositiveSportLayout];
    } else {
        [self setupNegativeSportLayout];
    }
}

/// 正向运动布局
- (void)setupPositiveSportLayout {
    
    // 更新绿球
    self.greenBallV.lc_centerX += BallSpeed;
    
    // 更新红球
    self.redBallV.lc_centerX  -= BallSpeed;
    
    // 缩放动画
    self.greenBallV.transform = [self largerTransformOfCenterX:self.redBallV.lc_centerX];
    self.redBallV.transform   = [self smallerTransformOfCenterX:self.redBallV.lc_centerX];

    // 更新黑球     相对于可见区域转换区域
    CGRect blackBallF = [self.redBallV convertRect:self.redBallV.bounds toCoordinateSpace:self.greenBallV];
    self.blackBallV.frame = blackBallF;
    self.blackBallV.layer.cornerRadius = self.blackBallV.lc_width/2;
    
    // 更新方向
    if (CGRectGetMaxX(self.greenBallV.frame) >= self.bgV.lc_width ||
        CGRectGetMinX(self.redBallV.frame) <= 0) {
        
        // 更改方向标签
        self.isDirection = NO;
        // 反向运动时  红球在上 绿球在下
        [self.bgV bringSubviewToFront:self.redBallV];
        // 黑球放在红球上面
        [self.redBallV addSubview:self.blackBallV];
        // 暂停一下
        [self pauseAnimation];
    }
}


/// 逆向运动
- (void)setupNegativeSportLayout {
    
    // 更新绿球
    self.greenBallV.lc_centerX -= BallSpeed;
    
    // 更新红球
    self.redBallV.lc_centerX += BallSpeed;
    
    // 缩放动画
    self.redBallV.transform   = [self largerTransformOfCenterX:self.redBallV.lc_centerX];
    self.greenBallV.transform = [self smallerTransformOfCenterX:self.redBallV.lc_centerX];
    
    // 更新黑球
    CGRect blackBallF = [self.greenBallV convertRect:self.greenBallV.bounds toCoordinateSpace:self.redBallV];
    self.blackBallV.frame = blackBallF;
    self.blackBallV.layer.cornerRadius = self.blackBallV.lc_width/2;
    
    // 更新方向
    if (CGRectGetMaxX(self.redBallV.frame) >= self.bgV.lc_width ||
        CGRectGetMinX(self.greenBallV.frame) <= 0) {
        
        // 更改方向标签
        self.isDirection = YES;
        // 反向运动时  红球在上 绿球在下
        [self.bgV bringSubviewToFront:self.greenBallV];
        // 黑球放在红球上面
        [self.greenBallV addSubview:self.blackBallV];
        // 暂停一下
        [self pauseAnimation];
    }
}

/// 暂停
- (void)pauseAnimation {
    
    self.displayLink.paused = YES;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(PauseSecond * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.displayLink.paused = NO;
    });
}

/// 放大动画
- (CGAffineTransform)largerTransformOfCenterX:(CGFloat)centerX {
    CGFloat cosValue = [self cosValueOfCenterX:centerX];
    return CGAffineTransformMakeScale(1 + cosValue*BallZoomScale, 1 + cosValue*BallZoomScale);
}

/// 缩小动画
- (CGAffineTransform)smallerTransformOfCenterX:(CGFloat)centerX {
    CGFloat cosValue = [self cosValueOfCenterX:centerX];
    return CGAffineTransformMakeScale(1 - cosValue*BallZoomScale, 1 - cosValue*BallZoomScale);
}

/// 根据余弦函数获取变化区间 变化范围是0~1~0
- (CGFloat)cosValueOfCenterX:(CGFloat)centerX {
    
    CGFloat apart = centerX - self.bgV.lc_width/2.0f;
    //最大距离(球心距离Container中心距离)
    CGFloat maxAppart = (self.bgV.lc_width - BallWidth)/2.0f;
    //移动距离和最大距离的比例
    CGFloat angle = apart/maxAppart*M_PI_2;
    //获取比例对应余弦曲线的Y值
    
    return cos(angle);
}


# 三. 使用


#import "LCDYLodingView.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [LCDYLodingView showLodingInView:self.view];
}


- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    
    [LCDYLodingView hideLodingInView:self.view];
}