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

最近在学习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");
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容