17-核心动画实战

转盘(旋转)

  • 自定义转盘上的button - WheelButton
    • 事件处理,重写hitTest方法来寻找最合适的view
// 寻找最合适的view
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 获取真实button的宽度和高度
    CGFloat btnW = self.bounds.size.width;
    CGFloat btnH = self.bounds.size.height;

    // 设置要处理事件的区域    
    CGFloat x = 0;
    CGFloat y = btnH / 2;
    CGFloat w = btnW;
    CGFloat h = y;
    CGRect rect = CGRectMake(x, y, w, h);
    if (CGRectContainsPoint(rect, point)) {
        return nil;
    }else{
        return [super hitTest:point withEvent:event];
    }
    
}
  • 自定义按钮可以控制按钮上图片显示的位置和尺寸
// 设置UIImageView的尺寸
// contentRect:按钮的尺寸
- (CGRect)imageRectForContentRect:(CGRect)contentRect
{
    // 计算UIImageView控件尺寸
    CGFloat imageW = 40;
    CGFloat imageH = 46;
    CGFloat imageX = (contentRect.size.width - imageW) * 0.5;
    CGFloat imageY = 20;
    return CGRectMake(imageX, imageY, imageW, imageH);
}
  • 自定义按钮取消点击时的高亮状态,需重写setHighlighted方法
// 取消高亮状态
- (void)setHighlighted:(BOOL)highlighted
{
    
}
  • 自定义转盘的view - WheelView
    • 在.h文件中声明初始化方法和start以及pause方法
+ (instancetype)wheelView;
- (void)start
- (void)pause
  • 在wheelView初始化方法中加载xib文件
+ (instancetype)wheelView
{
   return  [[NSBundle mainBundle] loadNibNamed:@"WheelView" owner:nil options:nil][0];
}
  • initWithCoder方法只是在加载xib的时候会调用,但是并不会将xib中的控件和代码声明进行连线
  • 可以在awakeFromNib方法中进行添加按钮的操作
- (void)awakeFromNib
{
    // 由于imageView的特殊性,默认是不能与用户进行交互的
    _centerView.userInteractionEnabled = YES;
    CGFloat btnW = 68;
    CGFloat btnH = 143;
    
    CGFloat wh = self.bounds.size.width;
    
    // 加载大图片(默认)
    UIImage *bigImage = [UIImage imageNamed:@"LuckyAstrology"];
    
    // 加载大图片(选中)
    UIImage *selBigImage = [UIImage imageNamed:@"LuckyAstrologyPressed"];
    
    // 获取当前使用的图片像素和点的比例
    CGFloat scale = [UIScreen mainScreen].scale;
    CGFloat imageW = bigImage.size.width / 12 * scale;
    CGFloat imageH = bigImage.size.height * scale;
    // CGImageRef image:需要裁减的图片
    // rect:裁减区域
    // 裁减区域是以像素为基准
    // CGImageCreateWithImageInRect(CGImageRef image, CGRect rect)
    
    // 添加按钮
    for (int i = 0; i < 12; i++) {
        WheelButton *btn = [WheelButton buttonWithType:UIButtonTypeCustom];
        
        // 设置按钮的位置
        btn.layer.anchorPoint = CGPointMake(0.5, 1);
        
        btn.bounds = CGRectMake(0, 0, btnW, btnH);
        
        btn.layer.position = CGPointMake(wh * 0.5, wh * 0.5);
        
        // 按钮的旋转角度
        CGFloat radion = (30 * i) / 180.0 * M_PI;
        
        btn.transform = CGAffineTransformMakeRotation(radion);
        
        [_centerView addSubview:btn];
        
        // 加载按钮的图片
        // 计算裁减区域
        CGRect clipR = CGRectMake(i * imageW, 0, imageW, imageH);
        
        // 裁减图片
        CGImageRef imgR =  CGImageCreateWithImageInRect(bigImage.CGImage, clipR);
        
        UIImage *image = [UIImage imageWithCGImage:imgR];
        
        // 设置按钮的图片
        [btn setImage:image forState:UIControlStateNormal];
        
        // 设置选中状态下图片
        imgR = CGImageCreateWithImageInRect(selBigImage.CGImage, clipR);

        image = [UIImage imageWithCGImage:imgR];
        
        // 设置按钮的图片
        [btn setImage:image forState:UIControlStateSelected];
        
        // 设置选中背景图片
        [btn setBackgroundImage:[UIImage imageNamed:@"LuckyRototeSelected"] forState:UIControlStateSelected];
        
        // 监听按钮的点击
        [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
        
        // 默认选中第一个
        if (i == 0) {
            [self btnClick:btn];
        }
        
        // btn.backgroundColor = [UIColor redColor];
    }
}
  • 默认记录上一次被点击的按钮,当点击下一个按钮时清空上一个按钮,并将当前按钮记录
- (void)btnClick:(UIButton *)btn
{
    _selBtn.selected = NO;
    btn.selected = YES;
    _selBtn = btn;
}
  • 当点击开始选号按钮时,通过transform获取选中按钮的旋转角度,并进行逆向旋转,并在动画结束以后重新开启定时器动画
#pragma mark - 点击开始选号的时候
- (IBAction)startPicker:(id)sender {
    
    // 不需要定时器旋转
    self.link.paused = YES;
    
    // 中间的转盘快速的旋转,并且不需要与用户交互
    
    CABasicAnimation *anim = [CABasicAnimation animation];
    
    anim.keyPath = @"transform.rotation";
    
    anim.toValue = @(M_PI * 2 * 3);
    
    anim.duration = 0.5;
    anim.delegate = self;
    
    [_centerView.layer addAnimation:anim forKey:nil];
    
    // 点击哪个星座,就把当前星座指向中心点上面
    
    // M_PI 3.14
    // 根据选中的按钮获取旋转的度数,
    // 通过transform获取角度
    CGFloat angle = atan2(_selBtn.transform.b, _selBtn.transform.a);
    
    // 旋转转盘
    _centerView.transform = CGAffineTransformMakeRotation(-angle);
    
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        self.link.paused = NO;
    });
}

折叠图片的动画

  • 原理分析:通过两个UIImageView分别显示图片的上半部分(topView)和下半部分(bottonView),然后将其放入同一个UIView(dragView),旋转的时候只旋转上部分的控件,为了让一张完整的图片通过两个控件显示,可以通过layer图层控制图片显示的内容

  • 如何快速的把两个控件拼接成一个完整的图片

    • 可以通过contentsRect设置图片显示的尺寸,取值0~1
    _topView.layer.contentsRect = CGRectMake(0, 0, 1, 0.5);
    _topView.layer.anchorPoint = CGPointMake(0.5, 1);
    
    _bottomView.layer.contentsRect = CGRectMake(0, 0.5, 1, 0.5);
    _bottomView.layer.anchorPoint = CGPointMake(0.5, 0);
  • 分别给dragView添加拖拽手势(UIPanGestureRecognizer)和bottonView添加渐变图层(CAGradientLayer)
    // 添加手势
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    
    [_dragView addGestureRecognizer:pan];
    
    // 渐变图层
    CAGradientLayer *gradientL = [CAGradientLayer layer];
    
    // 注意图层需要设置尺寸
    gradientL.frame = _bottomView.bounds;

    gradientL.opacity = 0;
    gradientL.colors = @[(id)[UIColor clearColor].CGColor,(id)[UIColor blackColor].CGColor];
    _gradientL = gradientL;
    // 设置渐变颜色
    // gradientL.colors = @[(id)[UIColor redColor].CGColor,(id)[UIColor greenColor].CGColor,(id)[UIColor yellowColor].CGColor];
    
    // 设置渐变定位点
    // gradientL.locations = @[@0.1,@0.4,@0.5];
    
    // 设置渐变开始点,取值0~1
    // gradientL.startPoint = CGPointMake(0, 1);
    
    [_bottomView.layer addSublayer:gradientL];
  • 在拖拽手势的pan方法中给topView的layer图层添加旋转动画以及设置渐变图层的阴影效果,并在手指抬起的时候添加弹簧效果的动画
// 拖动的时候旋转上部分内容,200 M_PI
- (void)pan:(UIPanGestureRecognizer *)pan
{
    // 获取偏移量
   CGPoint transP = [pan translationInView:_dragView];
    
    // 旋转角度,往下逆时针旋转
    CGFloat angle = -transP.y / 200.0 * M_PI;
    
    CATransform3D transfrom = CATransform3DIdentity;
    
    // 增加旋转的立体感,近大远小,d:距离图层的距离
    transfrom.m34 = -1 / 500.0;
    
    transfrom = CATransform3DRotate(transfrom, angle, 1, 0, 0);
    
    _topView.layer.transform = transfrom;
    
    // 设置阴影效果
    _gradientL.opacity = transP.y * 1 / 200.0;
    
    if (pan.state == UIGestureRecognizerStateEnded) { // 反弹
        
        // 弹簧效果的动画
        // SpringWithDamping:弹性系数,越小,弹簧效果越明显
        [UIView animateWithDuration:0.6 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:10 options:UIViewAnimationOptionCurveEaseInOut animations:^{
            
            _topView.layer.transform = CATransform3DIdentity;
            
        } completion:^(BOOL finished) {
            
        }];
    }
    
}

音量震动条的动画

  • 核心:复制图层CAReplicatorLayer的使用

  • 复制图层:是指可以把图层里面的所有子层复制

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    // CAReplicatorLayer复制图层,可以把图层里面所有子层复制
    // 创建复制图层
    CAReplicatorLayer *repL = [CAReplicatorLayer layer];
    
    repL.frame = _lightView.bounds;
    
    [_lightView.layer addSublayer:repL];
    
    CALayer *layer = [CALayer layer];
    
    layer.anchorPoint = CGPointMake(0.5, 1);
    layer.position = CGPointMake(15, _lightView.bounds.size.height);
    layer.bounds = CGRectMake(0, 0, 30, 150);
    
    layer.backgroundColor = [UIColor whiteColor].CGColor;
    
    [repL addSublayer:layer];
    
    CABasicAnimation *anim = [CABasicAnimation animation];
    
    anim.keyPath = @"transform.scale.y";
    
    anim.toValue = @0.1;
    
    anim.duration = 0.5;
    
    anim.repeatCount = MAXFLOAT;
    
    // 设置动画反转
    anim.autoreverses = YES;
    
    [layer addAnimation:anim forKey:nil];
    
    // 复制层中子层总数
    // instanceCount:表示复制层里面有多少个子层,包括原始层
    repL.instanceCount = 3;
    
    // 设置复制子层偏移量,不包括原始层,相对于原始层x偏移
    repL.instanceTransform = CATransform3DMakeTranslation(45, 0, 0);
    
    // 设置复制层动画延迟时间
    repL.instanceDelay = 0.1;
    
    // 如果设置了原始层背景色,就不需要设置这个属性
    repL.instanceColor = [UIColor greenColor].CGColor;
    
    repL.instanceGreenOffset = -0.3;
    
}

活动指示器动画

  • 同样是利用复制层制作类似进度条的动画
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    CAReplicatorLayer *repL = [CAReplicatorLayer layer];
    
    repL.frame = _redView.bounds;
    
    [_redView.layer addSublayer:repL];
    
    CALayer *layer = [CALayer layer];
    
    layer.transform = CATransform3DMakeScale(0, 0, 0);
    
    layer.position = CGPointMake(_redView.bounds.size.width / 2, 20);
    
    layer.bounds = CGRectMake(0, 0, 10, 10);
    
    layer.backgroundColor = [UIColor greenColor].CGColor;
    
    [repL addSublayer:layer];
    
    // 设置缩放动画
    CABasicAnimation *anim = [CABasicAnimation animation];
    
    anim.keyPath = @"transform.scale";
    
    anim.fromValue = @1;
    
    anim.toValue = @0;
    
    anim.repeatCount = MAXFLOAT;
    
    CGFloat duration = 1;
    
    anim.duration = duration;
    
    [layer addAnimation:anim forKey:nil];

    int count = 20;
    
    CGFloat angle = M_PI * 2 / count;
    
    // 设置子层总数
    repL.instanceCount = count;
    
    repL.instanceTransform = CATransform3DMakeRotation(angle, 0, 0, 1);
    
    repL.instanceDelay = duration / count;
    
}

单条路径粒子效果动画

  • 原理分析:在touchesBegan方法中创建UIBezierPath并设置起点,在touchesMoved方法添加线到某点,在awakeFromNib方法中初始化复制层和单个粒子的图层,当用户点击开始动画按钮,给单个粒子设置帧动画,并设置粒子的数量以及延迟动画的时间

  • 当手指触摸开始的时候

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    
    // 重绘
    [self reDraw];
    
    // 获取touch对象
    UITouch *touch = [touches anyObject];
    
    // 获取当前触摸点
    CGPoint curP = [touch locationInView:self];
    
    // 创建一个路径
    UIBezierPath *path = [UIBezierPath bezierPath];

    // 设置起点
    [path moveToPoint:curP];
    
    _path = path;
    
}
  • 在手指移动过程中时刻添加连线并进行重绘
static int _instansCount = 0;

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 获取touch对象
    UITouch *touch = [touches anyObject];
    
    // 获取当前触摸点
    CGPoint curP = [touch locationInView:self];

    // 添加线到某个点
    [_path addLineToPoint:curP];
    
    // 重绘
    [self setNeedsDisplay];
    
    _instansCount ++;
    
}

- (void)drawRect:(CGRect)rect {
    [_path stroke];
}
  • 在awakeFromNib方法中初始化图层
- (void)awakeFromNib
{
    // 创建复制层
    CAReplicatorLayer *repL = [CAReplicatorLayer layer];
    
    repL.frame = self.bounds;
    
    [self.layer addSublayer:repL];
    
    // 创建图层
    CALayer *layer = [CALayer layer];
    
    CGFloat wh = 10;
    layer.frame = CGRectMake(0, -1000, wh, wh);
    
    layer.cornerRadius = wh / 2;
    
    layer.backgroundColor = [UIColor blueColor].CGColor;
    
    [repL addSublayer:layer];
    
    _dotLayer = layer;
    
    _repL = repL;
}

#pragma mark - 开始动画
- (void)startAnim
{

    _dotLayer.hidden = NO;
    
    // 创建帧动画
    CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
    
    anim.keyPath = @"position";
    
    anim.path = _path.CGPath;
    
    anim.duration = 4;
    
    anim.repeatCount = MAXFLOAT;
    
    [_dotLayer addAnimation:anim forKey:nil];
    
    // 复制子层
    _repL.instanceCount = _instansCount;
    
    _repL.instanceDelay = 0.1;
    
}

- (void)reDraw
{
    
    _path = nil;
    [self setNeedsDisplay];
    
    _dotLayer.hidden = YES;
    
}

多条路径粒子效果动画

  • 原理分析:通过懒加载dotLayer和path,可以保证在程序运行过程中只有一个dotLayer和path对象

  • 懒加载dotLayer和path,需重写其get方法

#pragma mark - 懒加载点层
- (CALayer *)dotLayer
{
    if (_dotLayer == nil) {
        // 创建图层
        CALayer *layer = [CALayer layer];
        
        CGFloat wh = 10;
        layer.frame = CGRectMake(0, -1000, wh, wh);
        
        layer.cornerRadius = wh / 2;
        
        layer.backgroundColor = [UIColor blueColor].CGColor;
        [_repL addSublayer:layer];
        
        _dotLayer = layer;
    }
    return _dotLayer;
}

- (UIBezierPath *)path
{
    if (_path == nil) {
        _path = [UIBezierPath bezierPath];
    }
    
    return _path;
}
  • 注意:如果复制的子层有动画,需要先添加动画再复制,否则子层动画可能添加不成功

  • 复制子层:self.repL.instanceCount = self.instanceCount;

  • 延迟图层动画:self.repL.instanceDelay = 0.2;

倒影效果

  • 利用复制层,将图片绕X轴旋转后修改图层颜色通道的值
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    CAReplicatorLayer *layer =  (CAReplicatorLayer *)_repView.layer;
    
    layer.instanceCount = 2;
    
    CATransform3D transform = CATransform3DMakeTranslation(0, _repView.bounds.size.height, 0);
    // 绕着X轴旋转
    transform = CATransform3DRotate(transform, M_PI, 1, 0, 0);
    
    // 往下面平移控件的高度
    layer.instanceTransform = transform;
    
    layer.instanceAlphaOffset = -0.1;
    layer.instanceBlueOffset = -0.1;
    layer.instanceGreenOffset = -0.1;
    layer.instanceRedOffset = -0.1;

}

QQ粘性效果

  • 注意:touchesBegan方法会和按钮的监听事件冲突,所以在有按钮的监听事件以后,只能使用手势事件代替touchesBegan方法

  • 使用self.transform修改按钮的形变,并不会修改按钮的中心点,所以需要直接修改self.center

  • 每一次相对于上一次的形变,都需要进行复位操作

  • 绘制不规则的矩形,不能通过绘图,因为绘图只能在当前控件上画,超出部分将不会显示,而且只有当两个圆产生距离的时候才需要进行绘制

  • 描述两圆之间的矩形路径需要特定的算法

  • 手指抬起的时候,将大圆进行还原,根据圆心的距离判断是否需要移除不规则矩形

    • 当大小圆圆心的距离大于设定的最大圆心距离时,需要展示一张爆炸的gif图片,并将大圆从父控件中移除
    • 当圆心距离不大于规定的距离时,需要移除不规则矩形,并将大圆还原到默认的位置

你可能感兴趣的:(17-核心动画实战)