iOS渲染和动画iOS DeveloperiOS 开发

iOS弹簧动画的简单效果实现

2017-08-22  本文已影响1640人  ymdee

最近在学习CAAnimation,突发奇想想实现一个类似弹簧拉伸回弹的效果,如下图的效果:

demo2.gif

那么做之前先分析一下,实现这个效果需要:UIPanGestureRecognizer(控件拖动),CASpringAnimation(弹簧动画)。
给控件加上一个平移手势,让控件的center随手势在控件父类中的相对坐标改变而改变,这样就实现了控件的拖动。
放开控件的那一刻,我们需要创建一个CASpringAnimation,动画的fromValue 和toValue分别设置成控件的终点和起始点,把动画加到控件的layer上并把控件的frame设置成起始状态。如下代码:

#import "ViewController.h"@interface ViewController ()

@property (nonatomic, assign) CGPoint identifyPoint;

@property (nonatomic, strong) UIImageView *imageView;

@property (nonatomic, strong) CASpringAnimation *sp;

@end

@implementation ViewController

- (void)viewDidLoad {

  [super viewDidLoad];

  _identifyPoint = CGPointMake(200, 200);//起始点坐标

  _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];

  _imageView.center = _identifyPoint;

  _imageView.userInteractionEnabled = YES;

  _imageView.backgroundColor = [UIColor redColor];

  _imageView.layer.cornerRadius = 15;

  _imageView.layer.masksToBounds = YES;

  [self.view addSubview:_imageView];

  UIPanGestureRecognizer *pan =  [[UIPanGestureRecognizer alloc]initWithTarget:self   action:@selector(handlePanGestures:)];

  [_imageView addGestureRecognizer:pan];

}

-(void)handlePanGestures:(UIPanGestureRecognizer *)paramSender{

  if (paramSender.state != UIGestureRecognizerStateEnded && paramSender.state != UIGestureRecognizerStateFailed) {

    //拖动中

    CGPoint location = [paramSender locationInView:paramSender.view.superview];

    paramSender.view.center = location;

    [self.view setNeedsDisplay];

  }

  if (paramSender.state ==  UIGestureRecognizerStateEnded &&   paramSender.state !=   UIGestureRecognizerStateFailed) {

    //拖动完成回弹

    CGPoint location = [paramSender locationInView:paramSender.view.superview];

    paramSender.view.center = location;

    _sp = [CASpringAnimation animationWithKeyPath:@"position"];

    _sp.mass = 1;//质量,影响图层运动时的弹簧惯性,质量越大,弹簧拉伸和压缩的幅度越大,默认1

    _sp.stiffness = 100.0f;//弹性系数,弹性系数越大,形变产生的力就越大,运动越快,默认100

    _sp.damping = 5.0f;//阻尼系数,阻止弹簧伸缩的系数,阻尼系数越大,停止越快,默认10

    _sp.initialVelocity = 10.0f;//动画视图的初始速度大小,默认0

    _sp.duration = _sp.settlingDuration;//结算时间 返回弹簧动画到停止时的估算时间

    _sp.fillMode = kCAFillModeRemoved;//动画结束后复原

    _sp.autoreverses = NO;//不做逆动画

    _sp.removedOnCompletion = YES;//动画结束后移除

    _sp.fromValue = [NSValue valueWithCGPoint:location];

    _sp.toValue = [NSValue valueWithCGPoint:_identifyPoint];

    _imageView.center = _identifyPoint;//直接把控件设置到初始位置

    [_imageView.layer addAnimation:_sp forKey:@"spring"];

    [self.view setNeedsDisplay];

    }

  }

这种效果实现起来非常简单。那么接下来换成这种弯的效果怎么办呢?(线画的略丑,只是为了说明路径)


demo1.gif

分析:这种路径曲线光用CABasicAnimation(CASpringAnimation的父类就是它)是没有办法搞定的,只能采用关键帧动画,然而关键帧动画中是没有类似弹簧动画的属性的,所以我采用了CAKeyframeAnimation和CASpringAnimation结合的形式实现。我的实现思路是获取CAKeyframeAnimation最后0.2秒动画中起始点和结束点,设置成CASpringAnimation的fromValue和toValue,这样就相当于得到关键帧动画最后的移动方向(近似切线)。

#import "ViewController.h"
#import "Myview.h"

@interface ViewController () <CAAnimationDelegate>
@property (nonatomic, assign) CGFloat durtion;//keyframe动画持续时间
@property (nonatomic, assign) CGPoint identifyPoint;//起始点坐标
@property (nonatomic, strong) UIBezierPath *path;//滑动的路径path
@property (nonatomic, strong) NSMutableArray *pointArray;//所有路径点坐标数组
@property (nonatomic, strong) Myview *myview;
@property (nonatomic, strong) UIImageView *imageView;
@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _durtion = 2;//keyframe动画持续时间
    _identifyPoint = CGPointMake(200, 200);//起始点坐标
    
    _myview= [[Myview alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    _myview.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:_myview];
    
    _pointArray = [NSMutableArray array];
    _path = [[UIBezierPath alloc] init];
    _myview.path = _path;//为了画出路径的贝塞尔曲线,自己建了一个view重写了drawRect方法
    
    
    _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
    _imageView.center = _identifyPoint;
    _imageView.userInteractionEnabled = YES;
    _imageView.backgroundColor = [UIColor redColor];
    _imageView.layer.cornerRadius = 15;
    _imageView.layer.masksToBounds = YES;
    [_myview addSubview:_imageView];
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePanGestures:)];
    [_imageView addGestureRecognizer:pan];

}

-(void)handlePanGestures:(UIPanGestureRecognizer *)paramSender{
    if (paramSender.state == UIGestureRecognizerStateBegan && paramSender.state != UIGestureRecognizerStateFailed) {
        //开始滑动
        CGPoint location = [paramSender locationInView:paramSender.view.superview];//获取当前手势点在控件父类view中的相对坐标
        paramSender.view.center = location;//设置控件位置
        
        [_pointArray addObject:[NSValue valueWithCGPoint:_identifyPoint]];//把控件起点添加进数组
        [_path moveToPoint:_identifyPoint];//贝塞尔曲线起点
        [_myview setNeedsDisplay];//立即画出贝塞尔曲线
    }
    if (paramSender.state == UIGestureRecognizerStateChanged && paramSender.state != UIGestureRecognizerStateFailed) {
        //滑动中
        CGPoint location = [paramSender locationInView:paramSender.view.superview];
        paramSender.view.center = location;
        
        [_pointArray addObject:[NSValue valueWithCGPoint:location]];//把路径点添加进数组
        [_path addLineToPoint:location];//贝塞尔曲线添加路径
        [_myview setNeedsDisplay];
    }
    if (paramSender.state == UIGestureRecognizerStateEnded && paramSender.state != UIGestureRecognizerStateFailed) {
        //滑动结束
        CGPoint location = [paramSender locationInView:paramSender.view.superview];
        paramSender.view.center = location;
        [_path addLineToPoint:location];
        [_myview setNeedsDisplay];
        
        //下面的操作主要是为了获取一条与_path的相反方向的返回路径
        NSArray *dataArray=(NSMutableArray *)[[_pointArray reverseObjectEnumerator] allObjects];//翻转数组
        
        UIBezierPath *backpath = [[UIBezierPath alloc] init];
        backpath.lineCapStyle = kCGLineCapRound;
        backpath.lineJoinStyle = kCGLineJoinRound;
        int flag = 1;
        for (NSValue *value in dataArray) {
            CGPoint point = value.CGPointValue;
            if (flag) {
                [backpath moveToPoint:point];
                flag = 0;
            }else {
                [backpath addLineToPoint:point];
            }
        }
        
        //弹簧动画创建
        CASpringAnimation *sp = [CASpringAnimation animationWithKeyPath:@"position"];
        sp.mass = 1;
        sp.delegate = self;//设置代理,为了获取动画的开始结束状态
        sp.stiffness = 100.0f;
        sp.damping = 5.0f;
        sp.initialVelocity = 10.0f;
        sp.duration = sp.settlingDuration;
        sp.fillMode = kCAFillModeRemoved;
        sp.autoreverses = NO;
        sp.removedOnCompletion = YES;

        
        //获取动画最后的移动方向
        int i = ceilf(0.2 / _durtion * dataArray.count);
        sp.beginTime = CACurrentMediaTime() + (_durtion-0.2);
        
        NSValue *value1 = [dataArray lastObject];
        CGPoint lastPoint = value1.CGPointValue;
        
        NSValue *value2 = [dataArray objectAtIndex:(dataArray.count - 1 - i)];
        CGPoint startPoint = value2.CGPointValue;
        
        sp.fromValue = [NSValue valueWithCGPoint:startPoint];
        sp.toValue = [NSValue valueWithCGPoint:lastPoint];
        
        
        //关键帧动画创建
        CAKeyframeAnimation *ck = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        ck.repeatCount = 1;
        ck.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
        //  kCAMediaTimingFunctionEaseIn表示开始缓慢逐渐变快,这里符合弹簧动画的效果
        ck.path = backpath.CGPath;//设置成返回路径
        ck.duration = _durtion;
        ck.fillMode = kCAFillModeRemoved;//动画结束后复原
        ck.autoreverses = NO;//不做逆动画
        ck.removedOnCompletion = YES;//动画结束后移除
        //
        
        _imageView.center = _identifyPoint;
        [_imageView.layer addAnimation:sp forKey:@"spring"];
        [_imageView.layer addAnimation:ck forKey:@"keyframe"];
        [_myview setNeedsDisplay];
    }
}
        
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    //动画结束移除所有点信息
    [_path removeAllPoints];
    [_pointArray removeAllObjects];
    
    NSLog(@"animation finished");
}

这样效果就完成了,当然还存在一些瑕疵。如果有更好的实现方法也可以跟我交流,谢谢收看。

上一篇下一篇

猜你喜欢

热点阅读