核心动画(四)

关键帧动画实现围绕路径移动汽车

如果我们想让一个物体沿着一个轨迹移动,就需要用到UIBezierPath。使用UIBezierPath的两个关键点

  • 数据点(起点终点)、控制点
  • 为了显示贝塞尔曲线的路径,需要用到专门的图层CAShapeLayer

下面使用UIBezierPathCAShapeLayer实现一个汽车围绕路径移动的动画

#import "ViewController.h"

@interface ViewController ()
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self test];
}

- (void)test{
    //1.定义贝塞尔曲线
    UIBezierPath *path = [UIBezierPath bezierPath];
    //使用方法moveToPoint:去设置初始线段的起点
    [path moveToPoint:CGPointMake(20, 200)];
    //设置EndPoint & Control Point(终点和控制点)
    [path addCurveToPoint:CGPointMake(300, 200) controlPoint1:CGPointMake(100, 100) controlPoint2:CGPointMake(200, 300)];
    
    //2.为了显示贝塞尔曲线的路径 ->  CAShapeLayer专门用来显示UIBezierPath图层
    //CAShapeLayer 使用shapeLayer 可以更高效的渲染图形.并且不使用drawRect方法
    CAShapeLayer *shapeLyaer = [CAShapeLayer layer];
    //路径
    shapeLyaer.path = path.CGPath;
    //填充颜色
    //shapeLyaer.fillColor = [UIColor blueColor].CGColor;
    shapeLyaer.fillColor = nil;
    //线段颜色
    shapeLyaer.strokeColor = [UIColor redColor].CGColor;
    //为子图层添加贝塞尔曲线图
    [self.view.layer addSublayer:shapeLyaer];
    
    //3.添加图层
    CALayer *carLayer = [CALayer layer];
    carLayer.frame = CGRectMake(15, 200-18, 36, 36);
    //寄宿图
    carLayer.contents = (id)[UIImage imageNamed:@"car"].CGImage;
    carLayer.anchorPoint = CGPointMake(0.5, 0.8);
    [self.view.layer addSublayer:carLayer];
    
    //4.创建关键帧动画,让汽车沿着路径移动
    CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
    //路径
    anim.keyPath = @"position";
    //path
    anim.path = path.CGPath;
    //时长
    anim.duration = 4.0;
    //rotationMode
    anim.rotationMode = kCAAnimationRotateAuto;
    //为汽车图层添加动画
    [carLayer addAnimation:anim forKey:nil];
}

@end
汽车移动动画

物理引擎的简单使用

物理引擎是由UIKit提供的,负责了动画和交互的体系。UIDynamic引擎是iOS7.o引入的的技术,其目的是解放开发者,可以远离物理公式

UIDynamic物理引擎重要的类

  • 重力行为:UIGravityBehavior
  • 碰撞行为:UICollisionBehavior
  • 捕捉行为:UISnapBehavior
  • 推动行为:UIPushBehavior
  • 附着行为:UIAttchmentBehavior
  • 动力元素行为:UIDynamicItemBehavior

使用2D物理引擎的两个步骤

  • 添加行为,需要绑定view
  • 将行为添加到容器中

下面我们来使用一些常用的物理引擎,代码如下

#import "ViewController.h"

@interface ViewController ()
@property(nonatomic,strong)UIDynamicAnimator * animator;
@property(nonatomic,strong)UIDynamicAnimator * animator2;
@property(nonatomic,strong)UIAttachmentBehavior * attachmentBehavior;

@property(nonatomic,strong)UIImageView * redView;
@property(nonatomic,strong)UIImageView * greenView;
@property(nonatomic,strong)UIImageView * yellowView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //UI实现
    self.view.backgroundColor = [UIColor whiteColor];
   
    //红色球
    _redView =[[UIImageView alloc]initWithFrame:CGRectMake(100, 200, 50, 50)];
    _redView.image = [UIImage imageNamed:@"ball.png"];
    _redView.userInteractionEnabled = YES;
    _redView.backgroundColor =[UIColor redColor];
    _redView.layer.masksToBounds = YES;
    _redView.layer.cornerRadius = 25;
    [self.view addSubview:_redView];
    
    //绿色球
    _greenView =[[UIImageView alloc]initWithFrame:CGRectMake(100, 400, 50, 50)];
    _greenView.backgroundColor =[UIColor greenColor];
    _greenView.image = [UIImage imageNamed:@"ball2.png"];
    _greenView.userInteractionEnabled = YES;
    _greenView.layer.masksToBounds = YES;
    _greenView.layer.cornerRadius = 25;
    [self.view addSubview:_greenView];
    
    //黄色球
    _yellowView =[[UIImageView alloc]initWithFrame:CGRectMake(200, 500, 50, 50)];
    _yellowView.backgroundColor =[UIColor yellowColor];
    _yellowView.image = [UIImage imageNamed:@"ball3.png"];
    _yellowView.userInteractionEnabled = YES;
    _yellowView.layer.masksToBounds = YES;
    _yellowView.layer.cornerRadius = 25;
    [self.view addSubview:_yellowView];
    
    //物理引擎
    [self animator];
    
    //创建自由落体行为-重力,初始化哪几个view要接收这样的重力行为
    UIGravityBehavior *gravity = [[UIGravityBehavior alloc]initWithItems:@[_redView,_yellowView,_greenView]];
    //重力行为有一个属性是重力加速度,设置越大速度增长越快。默认是1
    gravity.magnitude = 2;
    //添加到容器
    [_animator addBehavior:gravity];
    
    //碰撞行为
     UICollisionBehavior *collision =[[UICollisionBehavior alloc]initWithItems:@[_redView,_yellowView,_greenView]];
    //设置边缘(父View的bounds)
    collision.translatesReferenceBoundsIntoBoundary = YES;
    
    //可以利用贝塞尔曲线限制边界
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
                          CGRectMake(0,150, self.view.frame.size.width, self.view.frame.size.width)];
    CAShapeLayer * shapeLayer =[CAShapeLayer layer];
    shapeLayer.path =path.CGPath;
    //画笔颜色
    shapeLayer.strokeColor =[UIColor redColor].CGColor;
    shapeLayer.lineWidth = 5;
    //填充颜色
    shapeLayer.fillColor = nil;
    [self.view.layer addSublayer:shapeLayer];
    [collision addBoundaryWithIdentifier:@"circle" forPath:path];
    [_animator addBehavior:collision];
    
    //模拟捕捉行为
    //捕捉行为需要在创建时就给与一个点
    //捕捉行为有一个防震系数属性,设置的越大,振幅就越小
    CGPoint point = CGPointMake(10, 400);
    UISnapBehavior *snap = [[UISnapBehavior alloc] initWithItem:_greenView snapToPoint:point];
    snap.damping = 1;
    [_animator addBehavior:snap];
    
    //其他行为的拓展
    UIDynamicItemBehavior *itemBehavior =[[UIDynamicItemBehavior alloc]initWithItems:@[_redView]];
    /*
     elasticity 弹性系数
     friction   摩擦系数
     density    密度
     resistance 抵抗性
     angularResistance 角度阻力
     charge     冲击
     anchored   锚定
     allowsRotation 允许旋转
     */
    itemBehavior.elasticity =.6;//弹性系数
    [_animator addBehavior:itemBehavior];
    
    //添加手势
    UIPanGestureRecognizer *pan =[[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panAuction:)];
    [_redView addGestureRecognizer:pan];
}

-(void)panAuction:(UIPanGestureRecognizer *)ges{
    if (ges.state == UIGestureRecognizerStateBegan) {
        UIOffset offset = UIOffsetMake(-10, -10);
        /*
         offsetFromCenter:偏离中心幅度
         attachedToAnchor:附加到锚点 手势点击的位置
         */
        //UIAttachmentBehavior 附着行为
        _attachmentBehavior =[[UIAttachmentBehavior alloc]initWithItem:_redView offsetFromCenter:offset attachedToAnchor:[ges locationInView:self.view]];
        
        [_animator addBehavior:_attachmentBehavior];
    }else if (ges.state == UIGestureRecognizerStateChanged){
        //设置锚点
        [_attachmentBehavior setAnchorPoint:[ges locationInView:self.view]];
    }else if (ges.state ==UIGestureRecognizerStateEnded || ges.state == UIGestureRecognizerStateFailed || ges.state == UIGestureRecognizerStateCancelled){
        [_animator removeBehavior:_attachmentBehavior];
    }
}

//懒加载
- (UIDynamicAnimator *)animator
{
    if (!_animator) {
        // 创建一个物理仿真器,关联到一个view上
        //容器(里面放一些行为)
        /*
         ReferenceView:关联的view
         */
        _animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
    }
    return _animator;
}

@end
引擎效果

足球从上面掉落下来把篮球撞击上去,同时我们给_redView足球添加了一个拖拽手势,可以把足球拖上去使其掉下来再次撞击篮球

下面我们给篮球添加一个捕捉行为,点击屏幕随意更改其位置

- (void)viewDidLoad {
    [super viewDidLoad];
    _animator2 = [[UIDynamicAnimator alloc]initWithReferenceView:self.view];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 1.获得手指对应的触摸对象
    UITouch *touch = [touches anyObject];
    
    // 2.获得触摸点
    CGPoint point = [touch locationInView:self.view];
    
    // 3.创建捕捉行为
    UISnapBehavior *snap = [[UISnapBehavior alloc] initWithItem:_yellowView snapToPoint:point];
    
    // 防震系数,damping越大,振幅越小
    snap.damping = 1;
    
    // 4.清空之前的并再次开始
    [_animator2 removeAllBehaviors];
    [_animator2 addBehavior:snap];
}

贝塞尔曲线

贝塞尔曲线(Bézier curve),又称⻉兹曲线贝济埃曲线,是应⽤于⼆维图形应用程序的数学曲线。一 般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的⽪筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线,在⼀些比较成熟的位图软件中也有贝塞尔曲线工具,如PhotoShop等。

贝塞尔曲线于1962,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用⻉塞尔曲线来为汽⻋的主体进行设计。⻉塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau演算法开发,以稳定数值的方法求出贝兹曲线。

贝塞尔曲线

贝塞尔曲线原理

UIBezierPath属性与方法
- 初始化方法
  初始化方法,需要用实例方法添加线条。使用比较多,可以根据需要任意定制样式,画任何我们想画的图形。
+ (instancetype)bezierPath;

  返回一个矩形path
+ (instancetype)bezierPathWithRect:(CGRect)rect;

  返回一个圆形或者椭圆形path
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;

  返回一个带圆角的矩形path,矩形的四个角都是圆角
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius;

 返回一个带圆角的矩形path,UIRectCorner 枚举值可以设置只绘制某个圆角
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;

 返回一段圆弧,参数说明:center:弧线中心点的坐标  radius:弧线所在圆的半径 startAngle:弧线开始的角度值 endAngle:弧线结束的角度值 clockwise:是否顺时针画弧线
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;

 用一条 CGpath 初始化
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;

 返回一个反转当前路径的路径对象(反方向绘制path)
- (UIBezierPath *)bezierPathByReversingPath API_AVAILABLE(ios(6.0));
属性
CGPath:将UIBezierPath类转换成CGPath
currentPoint:当前path的位置,可以理解为path的终点
lineWidth:线条宽度
lineCapStyle:端点样式
lineJoinStyle:连接类型
flatness:绘线的精细程度,默认为0.6,数值越大,需要处理的时间越长
usesEvenOddFillRule:判断奇偶数组的规则绘制图像,图形复杂时填充颜色的一种规则。类似棋盘
miterLimit:最大斜接长度 (只有在使用kCGLineJoinMiter时才有效,最大限制为10) ,边角的角度越小,斜接长度就会越大,为了避免斜接长度过长,使用lineLimit属性限制,如果斜接长度超过miterLimit,边角就会以KCALineJoinBevel类型来显示
- setLineDash: count: phase: 为path绘制虚线,dash数组存放各段虚线的长度,count是数组元素数量,phase是起始位置

lineCapStyle - 端点类型
kCGLineCapButt:无端点
kCGLineCapRound:圆形端点
kCGLineCapSquare:方形端点(样式上和kCGLineCapButt是一样的,但是比kCGLineCapButt长一点)

lineJoinStyle - 交叉点的类型
kCGLineJoinMiter:尖角衔接
kCGLineJoinRound:圆角衔接
kCGLineJoinBevel:斜角衔接
UIBezierPath构建Path
 以point点开始作为起点,一般用 + (instancetype)bezierPath 创建的贝塞尔曲线,先用该方法标注一个起点,再调用其他的创建线条的方法来绘制曲线
- (void)moveToPoint:(CGPoint)point;

 绘制二次贝塞尔曲线的关键方法,即从path的最后一点开始添加一条线到point点
- (void)addLineToPoint:(CGPoint)point;

 绘制二次贝塞尔曲线的关键方法,和 -moveToPoint: 配合使用。endPoint为终止点,controlPoint为控制点
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;

 绘制三次贝塞尔曲线的关键方法,以三个点画一段曲线。一般和 moveToPoint: 配合使用。
 其中起始点由 -moveToPoint: 设置,终止点为 endPoint:,控制点1的坐标controlPoint1,控制点2的坐标是controlPoint2。
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;

 绘制一段圆弧,center: 原点坐标  radius: 半径   startAngle: 起始角度  endAngle: 终止角度  clockwise 顺时针/逆时针方向绘制
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;

 闭合线
- (void)closePath;

 移除所有的点,从而有效的删除所有子路径
- (void)removeAllPoints;

 追加指定的bezierPath到路径上
- (void)appendPath:(UIBezierPath *)bezierPath;

 用仿射变换矩阵变换路径的所有点
- (void)applyTransform:(CGAffineTransform)transform;
图形上下文中的路径操作
 填充路径
- (void)fill;

 各个点连线
- (void)stroke;

 填充模式,alpha 设置
 blendMode : https://onevcat.com/2013/04/using-blending-in-ios/
- (void)fillWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;

 链接模式,alpha设置
- (void)strokeWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
 
 图形绘制超出当前路径范围,则不可见
- (void)addClip;

QQ消息提示按钮实现

下面我们使用UIBezierPath实现一个QQ消息提示按钮的拖拽效果

消息提示按钮拖拽
拆分动画:
  • 2个圆(一个固定圆,一个拖拽圆)
  • 贝塞尔曲线,求得关键点.
  • 固定圆比例缩小
  • 拖拽到一定距离的时候需要断开
  • 断开之后有个圆的反弹效果bgg
实现思路拆分步骤(根据两个圆求得关键点)
获取斜边d以及角度
已知条件
计算A点

注意:计算A点坐标的时候,由于屏幕左上角是原点(0, 0),所以要用(x1, y1)坐标分别减去对应的间距。

计算B点
计算C点
计算D点
计算O点
计算P点
#import "ViewController.h"

@interface ViewController ()
//圆1
@property (nonatomic, strong) UIView *view1;
//圆2
@property (nonatomic, strong) UIView *view2;
//shapeLayer图层
@property (nonatomic, strong) CAShapeLayer *shapeLayer;
//坐标记录
@property (nonatomic, assign) CGPoint oldViewCenter;
@property (nonatomic, assign) CGRect oldViewFrame;
@property (nonatomic, assign) CGFloat r1;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //1.UI.
    [self setUp];
}

-(void)setUp
{
    //添加view1
    _view1 = [[UIView alloc] initWithFrame:CGRectMake(36, CGRectGetHeight(self.view.bounds)-66, 40, 40)];
    _view1.layer.cornerRadius = 20;
    _view1.backgroundColor = [UIColor redColor];
    [self.view addSubview:_view1];
    //添加view2
    _view2 = [[UIView alloc] initWithFrame:_view1.frame];
    _view2.layer.cornerRadius = 20;
    _view2.backgroundColor = [UIColor redColor];
    [self.view addSubview:_view2];
    //添加label
    UILabel *numL = [[UILabel alloc] initWithFrame:_view2.bounds];
    numL.text = @"99";
    numL.textAlignment = NSTextAlignmentCenter;
    numL.textColor = [UIColor whiteColor];
    [_view2 addSubview:numL];
    //添加手势
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
    [_view2 addGestureRecognizer:pan];
    
    //初始化layer
    _shapeLayer = [CAShapeLayer layer];
    _oldViewFrame = _view1.frame;
    _oldViewCenter = _view1.center;
    _r1 = CGRectGetWidth(_view1.frame)/2;
}

-(void)panAction:(UIPanGestureRecognizer *)ges{
    if (ges.state == UIGestureRecognizerStateChanged) {
        //1.view2跟着手指移动 --执行案例
        _view2.center = [ges locationInView:self.view];
        
        //当拖拽到一定距离之后,则移出来
        //那么就是r1半径缩减到一定距离
        //在caculPoint 方法中,更新r1值
        //再拖拽if中判断,如果低于9
        if (_r1 < 9) {
            _view1.hidden = YES;
            [_shapeLayer removeFromSuperlayer];
            
        }
        //2.计算6个关键点,并画出贝塞尔曲线
        [self caculPoint];
    }else if(ges.state == UIGestureRecognizerStateEnded
             || ges.state == UIGestureRecognizerStateFailed
             || ges.state == UIGestureRecognizerStateCancelled)
    {
        //思考: 回弹时,那些属性/组件要做调整
        //view2 位置恢复,shaperLayer消失,
       // [_shapeLayer removeFromSuperlayer];
       // _view2.center= _oldViewCenter;
        
        [_shapeLayer removeFromSuperlayer];
        //加上弹跳效果动画
        __weak typeof(self) weakSelf = self;
        [UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
            weakSelf.view2.center = weakSelf.oldViewCenter;
        } completion:^(BOOL finished) {
            weakSelf.view1.hidden = NO;
            weakSelf.view1.frame = weakSelf.oldViewFrame;
            weakSelf.r1 = weakSelf.oldViewFrame.size.width/2;
            weakSelf.view1.layer.cornerRadius = weakSelf.r1;
        }];
    }
}

-(void)caculPoint{
    //1.初始化已知顶点
    CGPoint center1 = _view1.center;
    CGPoint center2 = _view2.center;
    
    //2.计算出斜边d的长度(根据勾股定理)
    //d= √((x2-x1)•(x2-x1) + (y1-y2)•(y1-y2));
    CGFloat dis = sqrtf(pow((center2.x-center1.x), 2)+pow(center1.y-center2.y, 2));
    
    //3.计算sin(正弦),cos(余弦)数据
    CGFloat sinValue = (center2.x - center1.x)/dis;
    CGFloat cosValue = (center1.y - center2.y)/dis;
    
    //4.半径
    CGFloat r1 = CGRectGetWidth(_oldViewFrame)/2 - dis/20;
    CGFloat r2 = CGRectGetHeight(_view2.bounds)/2;
    //更新_r1值
    _r1 = r1;
    
    //5.计算6个关键点
    CGPoint pA = CGPointMake(center1.x - r1 * cosValue, center1.y - r1 * sinValue);
    CGPoint pB = CGPointMake(center1.x + r1 * cosValue, center1.y + r1 * sinValue);
    CGPoint pC = CGPointMake(center2.x + r2 * cosValue, center2.y + r2 * sinValue);
    CGPoint pD = CGPointMake(center2.x - r2 * cosValue, center2.y - r2 * sinValue);
    
    CGPoint pO = CGPointMake(pA.x + dis/2*sinValue, pA.y - dis/2*cosValue);
    CGPoint pP = CGPointMake(pB.x + dis/2*sinValue, pB.y - dis/2*cosValue);
    
    //6.绘制贝塞尔曲线
    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:pA];
    [path addQuadCurveToPoint:pD controlPoint:pO];
    [path addLineToPoint:pC];
    [path addQuadCurveToPoint:pB controlPoint:pP];
    [path closePath];
    
    //7.把路径添加layer
    //这些写不可以,因为每一次拖拽就会新建一个CAShapeLayer
    /*
    CAShapeLayer *sh = [[CAShapeLayer alloc]init];
    sh.path = path.CGPath;
    sh.fillColor = [UIColor redColor].CGColor;
    //8.将layer添加到view上--执行代码
    [self.view.layer insertSublayer:sh above:_view2.layer];
    */
    
    if (_view1.hidden) {
        return;
    }
    
    _shapeLayer.path = path.CGPath;
    _shapeLayer.fillColor = [UIColor redColor].CGColor;
    [self.view.layer insertSublayer:_shapeLayer below:_view2.layer];
    
    //8.重新计算view1的位置---执行代码.
    _view1.center = _oldViewCenter;
    _view1.bounds = CGRectMake(0, 0, r1*2, r1*2);
    _view1.layer.cornerRadius = r1;
    //发生错误! 修改r1的计算才能正常显示
    //CGFloat r1 = CGRectGetWidth(_oldViewFrame)/2 - dis/20;
}

@end

你可能感兴趣的:(核心动画(四))