最近在学习CAAnimation,突发奇想想实现一个类似弹簧拉伸回弹的效果,如下图的效果:
那么做之前先分析一下,实现这个效果需要: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];
}
}
这种效果实现起来非常简单。那么接下来换成这种弯的效果怎么办呢?(线画的略丑,只是为了说明路径)
分析:这种路径曲线光用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");
}
这样效果就完成了,当然还存在一些瑕疵。如果有更好的实现方法也可以跟我交流,谢谢收看。