2018-07-05 17:08:50 u010960265 阅读数 846


1. iOS动画

总的来说,从涉及类的形式来看,iOS动画有:基于UIView的仿射形变动画,基于CAAnimation及其子类的动画,基于CG的动画。这篇文章着重总结前两种动画。

2. UIView动画

设置UIView形变动画有两种常见用到的属性,.frame,.transform,所以有的人也可以分别称之为:

① frame动画
② transform动画

这两种动画只需要在动画语法中适当的位置,基于UIView和CALayer的属性设置变化值即可。这种动画,不需要 调用核心动画CAAnimation里面的专用类和API。

其中,frame动画设置方式有限,必须确切地制定形变前后的frame,平移还好,特别是 旋转 的时候,只能通过数学知识计算出新的frame。这就得不偿失了。所以,更多的时候,当涉及一些frame,bounds,center的改变或是形变的时候可以用transform来取代frame。

2.1 设置UIView动画的两种语法形式

begin — commit

//偏移动画
  [UIView beginAnimations:@"move" context:nil];  
  [UIView setAnimationDuration:2];  
  [UIView setAnimationDelegate:self];  
  imageContainView.frame = CGRectMake(80, 80, 200, 200);  
  [label1 setBackgroundColor:[UIColor yellowColor]];  
  [label1 setTextColor:[UIColor redColor]];  
  [UIView commitAnimations]; 

animations block

//缩放动画
  view.transform = CGAffineTransformIdentity;
  [UIView animateWithDuration:1.0f animations:^{
     view.transform = CGAffineTransformMakeScale(2.0f, 2.0f);
  }];

2.2 设置属性形变动画的两种类型

UIView的 CGAffineTransform 类型属性:animatedView.transform
一般是View的旋转,拉伸移动等属性,是二维的,通常使用都是前缀CGAffineTransform的类。

CGAffineTransform transform = CGAffineTransformScale(imageContainView.transform, 1.2, 1.2); 
[UIView beginAnimations: @"scale"context: nil]; 
[UIView setAnimationDuration: 2];
[UIView setAnimationDelegate: self];
[imageView setTransform: transform]; 
[UIView commitAnimations];

CALayer的CATransform3D 类型属性:animaView.layer.transform
通过 .layer.transform 可以在3D模式下面的变化,通常使用的都是前缀为CATransform3D的类。

imageView.layer.transform =  CATransform3DIdentity;
[UIView animateWithDuration:1.0f animations:^{
      imageView.layer.transform = CATransform3DMakeScale(2.0, 2.0, 1.0);
}];

2.3 与动画相关的属性

2.3.1 UIView与动画相关的属性–与CGAffineTransform对应

下面是UIView的一些属性介绍

@property(nonatomic) CGRect            frame;
@property(nonatomic) CGRect            bounds;      // default bounds is zero origin, frame size. animatable
@property(nonatomic) CGPoint           center;      // center is center of frame. animatable
@property(nonatomic) CGAffineTransform transform;   // default is CGAffineTransformIdentity. animatable
@property(nonatomic) CGFloat           contentScaleFactor NS_AVAILABLE_IOS(4_0);

@property(nonatomic,getter=isMultipleTouchEnabled) BOOL multipleTouchEnabled __TVOS_PROHIBITED;   // default is NO
@property(nonatomic,getter=isExclusiveTouch) BOOL       exclusiveTouch __TVOS_PROHIBITED;         // default is NO

在实际开发中,使用场景:

(1) 当涉及一些frame, bounds, center的改变或是形变的时候可以用 transform 来取代 frame。
(2) 一般在实际开发中都是平移,旋转,缩放组合使用。

######2.3.2 CALayer与动画相关的属性–与CATransform3D对应
下面是CALayer的一些属性介绍

//宽度和高度
@property CGRect bounds;

//位置(默认指中点,具体由anchorPoint决定)
@property CGPoint position;

//锚点(x,y的范围都是0-1),决定了position的含义
@property CGPoint anchorPoint;

//背景颜色(CGColorRef类型)
@property CGColorRef backgroundColor;

//形变属性
@property CATransform3D transform;

//边框颜色(CGColorRef类型)
@property CGColorRef  borderColor;

//边框宽度
@property CGFloat borderWidth;

//圆角半径
@property CGFloat cornerRadius;

//内容(比如设置为图片CGImageRef)
@property(retain) id contents;

2.4 管理二维形变和三维形变的封装类:CGAffineTransform与CATransform3D

2.4.1 CGAffineTransform操作API

CGAffineTransform结构体定义


struct CGAffineTransform {
  CGFloat a, b, c, d;
  CGFloat tx, ty;
};

它其实表示的是一个矩阵:
这里写图片描述

因为最后一列总是是(0,0,1),所以有用的信息就是前面两列。对一个view进行仿射变化就相当于对view上的每个点做一个乘法,结果就是:

这里写图片描述

a表示x水平方向的缩放,tx表示x水平方向的偏移
d表示y垂直方向的缩放,ty表示y垂直方向的偏移
如果b和c不为零的话,那么视图肯定发生了旋转,旋转角度这样计算:tan(angle) = b / a

如果这样:

这里写图片描述

这个就是没有变化的最初的样子。

CGAffineTransform操作API

//还原
CGAffineTransformIdentity

//位移仿射  ---- 理解为平移 (CGFloat tx,CGFloat ty) 
CGAffineTransformMakeTranslation 
CGAffineTransformTranslate 
//旋转仿射 ---- 理解为旋转 (CGFloat angle)
CGAffineTransformMakeRotation
CGAffineTransformRotate 
//缩放仿射  --- 理解缩放大小 (CGFloat sx, CGFloat sy)
CGAffineTransformMakeScale 
CGAffineTransformScale

CGAffineTransform操作的数学本质

CGPoint转换公式

这里写图片描述

矩阵乘法运算原理演示

这里写图片描述

2.4.2 CATransform3D操作API
//还原
 CATransform3DIdentity

 //位移3D仿射  ==> (CGFloat tx, CGFloat ty, CGFloat tz)
CATransform3DMakeTranslation
CATransform3DTranslation        
//旋转3D仿射 ==> (CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeRotation
CATransform3DRotation  
//缩放3D仿射 ==>  (CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale
CATransform3DScale
//叠加3D仿射效果
CATransform3DConcat    
//仿射基础3D方法,可以直接做效果叠加
CGAffineTransformMake (sx,shx,shy,sy,tx,ty)
//检查是否有做过仿射3D效果  == ((CATransform3D t))
CATransform3DIsIdentity(transform)
//检查2个3D仿射效果是否相同
CATransform3DEqualToTransform(transform1,transform2)
//3D仿射效果反转(反效果,比如原来扩大,就变成缩小)
CATransform3DInvert(transform)
2.4.3 CATransform3D与CGAffineTransform相互转换API
//将一个CGAffinrTransform转化为CATransform3D
CATransform3D CATransform3DMakeAffineTransform (CGAffineTransform m);
//判断一个CATransform3D是否可以转换为CAAffineTransformbool 
CATransform3DIsAffine (CATransform3D t);
//将CATransform3D转换为CGAffineTransform
CGAffineTransform CATransform3DGetAffineTransform (CATransform3D t);

2.5 “组合动画” 与 CGAffineTransformConcat

2.5.1 连接设置多个属性组合成一个动画

连接设置两个以上属性的动画,可以先调用含有 formMake 的API,然后再调用只含 form 的API。例如,这样:

alertView.transform = CGAffineTransformMakeScale(.25, .25);
alertView.transform = CGAffineTransformTranslate(alertView.transform, 0, 600);
2.5.2 利用CGAffineTransformConcat设置组合动画

另外,可以直接利用 CGAffineTransformConcat 来组合多种含有 formMake 的形变API。

CGAffineTransform viewTransform = CGAffineTransformConcat(CGAffineTransformMakeScale(.25, .25), CGAffineTransformMakeTranslation(0, 600));
alertView.transform = viewTransform;

关于组合3D形变也有相应的API — CATransform3DConcat,关于3D形变下一篇会专门介绍。

CGAffineTransformConcat 组合多种形变动画小例子

- (void)viewDidAppear: (BOOL)animated {
    ///  初始化动画开始前label的位置
    CGFloat offset = label1.frame.size.height * 0.5;

    label1.transform = CGAffineTransformConcat(
      CGAffineTransformMakeScale(0, 0),
      CGAffineTransformTranslate(0, -offset)
    );
    label1.alpha = 0;
    [UIView animateWithDuration: 3. animations: ^ {
        ///  还原label1的变换状态并形变和偏移label2
        label1.transform = CGAffineTransformIdentifier;
        label1.transform = CGAffineTransformConcat(
          CGAffineTransformMakeScale(0, 0),
          CGAffineTransformTranslate(0, offset)
        );
        label1.alpha = 1;
        label2.alpha = 0;
    }];
}

组合变换的本质
CGAffineTransformConcat的数学本质是将括号内代表的若干变换的系数矩阵进行相乘。

另一种组合变换
基于已有的CGAffineTransform连续追加新的CGAffineTransform:

CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 0.5f, 0.5f);
transform = CGAffineTransformRotate(transform, 30.0f/180.0f*M_PI);
transform = CGAffineTransformTranslate(transform, 200.0f, 0.0f);
layerView.layer.affineTransform = transform;

2.6 动画后将属性还原

当我们改变过一个view.transform属性或者view.layer.transform的时候需要恢复默认状态的话,记得先把他 们重置为:

view.transform = CGAffineTransformIdentity;
view.layer.transform = CATransform3DIdentity;

2.7 注意点: transform对frame的影响

官方文档上关于transform属性的说明:

Changes to this property can be animated. However, if the transform
property contains a non-identity transform, the value of the frame
property is undefined and should not be modified. In that case, you
can reposition the view using the center property and adjust the size
using the bounds property instead.

如果在程序中改变了某个控件的transform,那么请不要使用这个控件的frame计算 子控件 的布局,应该使用bounds+center代替。

3. CAAnimation核心动画

CAAnimation——所有动画对象的父类

3.1 设置动画的一种语法形式

addAnimation

/**
 *  抖动效果
 */
-(void)shakeAnimation{
    CAKeyframeAnimation *anima = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];//在这里@"transform.rotation"==@"transform.rotation.z"
    NSValue *value1 = [NSNumber numberWithFloat:-M_PI/180*4];
    NSValue *value2 = [NSNumber numberWithFloat:M_PI/180*4];
    NSValue *value3 = [NSNumber numberWithFloat:-M_PI/180*4];
    anima.values = @[value1,value2,value3];
    anima.repeatCount = MAXFLOAT;

    [_demoView.layer addAnimation:anima forKey:@"shakeAnimation"];
}

3.2 CAAnimation继承结构

CAAnimation{
     CAPropertyAnimation{
            CABasicAnimation{
                    CASpringAnimation
            }
            CAKeyframeAnimation
     }
     CATransition   
     CAAnimationGroup
}

是所有动画对象的父类,负责控制动画的持续时间和速度,是个抽象类,不能直接使用,应该使用它具体的子类

3.3 CAAnimation类的属性

带*号代表来自CAMediaTiming协议的属性)

  • *duration:动画的持续时间
  • *repeatCount:重复次数,无限循环可以设置HUGE_VALF或者MAXFLOAT
  • *repeatDuration:重复时间
  • removedOnCompletion:默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillMode为kCAFillModeForwards
  • *fillMode:决定当前对象在非active时间段的行为。比如动画开始之前或者动画结束之后
  • *beginTime:可以用来设置动画延迟执行时间,若想延迟2s,就设置为
  • CACurrentMediaTime()+2,CACurrentMediaTime()为图层的当前时间
  • timingFunction:速度控制函数,控制动画运行的节奏
  • delegate:动画代理

3.4 几个重要属性值

removedOnCompletion属性值

CAAnimation——动画填充模式

默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillMode为kCAFillModeForwards

fillMode属性值

CAAnimation——控制恢复到动画执行前

要想fillMode有效,最好设置removedOnCompletion = NO

  • kCAFillModeRemoved
    这个是默认值,也就是说当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态
  • kCAFillModeForwards 当动画结束后,layer会一直保持着动画最后的状态
  • kCAFillModeBackwards
    在动画开始前,只需要将动画加入了一个layer,layer便立即进入动画的初始状态并等待动画开始。
  • kCAFillModeBoth
    这个其实就是上面两个的合成.动画加入后开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态

如果 fillMode = kCAFillModeForwards 同时 removedOnComletion = NO ,那么在动画执行完毕后,图层会保持显示动画执行后的状态。但在实质上,图层的属性值还是动画执行前的初始值,并没有真正被改变。

timingFunction属性值

CAAnimation——动画速度控制函数

  • kCAMediaTimingFunctionLinear(线性):匀速,给你一个相对静态的感觉
  • kCAMediaTimingFunctionEaseIn(渐进):动画缓慢进入,然后加速离开
  • kCAMediaTimingFunctionEaseOut(渐出):动画全速进入,然后减速的到达目的地
  • kCAMediaTimingFunctionEaseInEaseOut(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。

4. CABasicAnimation

基本动画,是CAPropertyAnimation的子类

4.1 特别属性说明:

  • keyPath: 要改变的属性名称(传字符串)
  • fromValue: keyPath相应属性的初始值
  • toValue: keyPath相应属性的结束值

4.2 keyPath可以是哪些值

CATransform3D{
    //rotation旋转
    transform.rotation.x
    transform.rotation.y
    transform.rotation.z

    //scale缩放
    transform.scale.x
    transform.scale.y
    transform.scale.z

    //translation平移
    transform.translation.x
    transform.translation.y
    transform.translation.z
}

CGPoint{
    position
    position.x
    position.y
}

CGRect{
    bounds
    bounds.size
    bounds.size.width
    bounds.size.height

    bounds.origin
    bounds.origin.x
    bounds.origin.y
}

property{
    opacity
    backgroundColor
    cornerRadius
    borderWidth
    contents

    Shadow{
        shadowColor
        shadowOffset
        shadowOpacity
        shadowRadius
    }
}

4.3 举例

缩放动画 – transform.scale

//心脏缩放动画
    CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; //选中的这个keyPath就是缩放
    scaleAnimation.fromValue = [NSNumber numberWithDouble:0.5]; //一开始时是0.5的大小
    scaleAnimation.toValue = [NSNumber numberWithDouble:1.5];  //结束时是1.5的大小
    scaleAnimation.duration = 1; //设置时间
    scaleAnimation.repeatCount = MAXFLOAT; //重复次数
    [_heartImageView.layer addAnimation:scaleAnimation forKey:@"CQScale"]; //添加动画

旋转动画 – transform.rotation.z

 //风车旋转动画
    CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    rotationAnimation.fromValue = [NSNumber numberWithDouble:0.f];
    rotationAnimation.toValue = [NSNumber numberWithDouble:2 * M_PI];
    rotationAnimation.duration = 2.f;
    rotationAnimation.repeatCount = MAXFLOAT;
    [_fengcheImageView.layer addAnimation:rotationAnimation forKey:@"CQRotation"];

平移动画 – position.x/position.y

 //平移动画
    CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position.x"];
    positionAnimation.fromValue = [NSNumber numberWithDouble:0.f];
    positionAnimation.toValue = [NSNumber numberWithDouble:SCREEN_WIDTH];
    positionAnimation.duration = 2;
    positionAnimation.repeatCount = MAXFLOAT;
    [_arrowImageView.layer addAnimation:positionAnimation forKey:@"CQPosition"];

5. CAKeyframeAnimation关键帧动画

5.1 参数数组形式

//根据values移动的动画
    CAKeyframeAnimation *catKeyAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    CGPoint originalPoint = self.catImageView.layer.frame.origin;
    CGFloat distance =  50;
    NSValue *value1 = [NSValue valueWithCGPoint:CGPointMake(originalPoint.x + distance, originalPoint.y + distance)];
    NSValue *value2 = [NSValue valueWithCGPoint:CGPointMake(originalPoint.x + 2 * distance, originalPoint.y + distance)];
    NSValue *value3 = [NSValue valueWithCGPoint:CGPointMake(originalPoint.x + 2 * distance, originalPoint.y +  2 * distance)];
    NSValue *value4 = [NSValue valueWithCGPoint:originalPoint];
    catKeyAnimation.values = @[value4, value1, value2, value3, value4];
    catKeyAnimation.duration = 2;
    catKeyAnimation.repeatCount = MAXFLOAT;
    catKeyAnimation.removedOnCompletion = NO;
    [self.catImageView.layer addAnimation:catKeyAnimation forKey:nil];

5.2 path形式

//指定path
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 200, 200, 200)];
    //指定path的动画
    UIBezierPath *path2 = [UIBezierPath bezierPath];
    [path2 moveToPoint:CGPointMake(100, 100)];
    [path2 addLineToPoint:CGPointMake(100, 200)];
    [path2 addLineToPoint:CGPointMake(200, 200)];
    [path2 addLineToPoint:CGPointMake(200, 100)];
    [path2 addLineToPoint:CGPointMake(100, 100)];
    CAKeyframeAnimation *penguinAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    penguinAnimation.path = path2.CGPath;
    penguinAnimation.duration = 2;
    penguinAnimation.repeatCount = MAXFLOAT;
    penguinAnimation.removedOnCompletion = NO;
    [self.penguinImageView.layer addAnimation:penguinAnimation forKey:nil];

6. 组动画

6.1 组动画

上面单一动画的情况在实际开发中实际比较少,更多的时候是组合这些动画:创建不同类型的动画对象,设置好它们的参数,然后把这些动画对象存进数组,传进组动画对象的animations属性中去。

6.2 示例

 //创建组动画
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.duration = 3;
    animationGroup.repeatCount = MAXFLOAT;
    animationGroup.removedOnCompletion = NO;
    /* beginTime 可以分别设置每个动画的beginTime来控制组动画中每个动画的触发时间,时间不能够超过动画的时间,默认都为0.f */

    //缩放动画
    CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
    animation1.values = @[[NSNumber numberWithFloat:1.0],[NSNumber numberWithFloat:0.5],[NSNumber numberWithFloat:1.5],[NSNumber numberWithFloat:1.0]];
    animation1.beginTime = 0.f;

    //按照圆弧移动动画
    CAKeyframeAnimation *animation2 = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    [bezierPath moveToPoint:CGPointMake(300, 200)];
    [bezierPath addQuadCurveToPoint:CGPointMake(200, 300) controlPoint:CGPointMake(300, 300)];
    [bezierPath addQuadCurveToPoint:CGPointMake(100, 200) controlPoint:CGPointMake(100, 300)];
    [bezierPath addQuadCurveToPoint:CGPointMake(200, 100) controlPoint:CGPointMake(100, 100)];
    [bezierPath addQuadCurveToPoint:CGPointMake(300, 200) controlPoint:CGPointMake(300, 100)];
    animation2.path = bezierPath.CGPath;
    animation2.beginTime = 0.f;

    //透明度动画
    CABasicAnimation *animation3 = [CABasicAnimation animationWithKeyPath:@"opacity"];
    animation3.fromValue = [NSNumber numberWithDouble:0.0];
    animation3.toValue = [NSNumber numberWithDouble:1.0];
    animation3.beginTime = 0.f;

    //添加组动画
    animationGroup.animations = @[animation1, animation2,animation3];
    [_penguinImageView.layer addAnimation:animationGroup forKey:nil];

7. 贝塞尔曲线

前面关键帧动画章节提到了贝塞尔曲线,这个曲线很有用,在iOS开发中有两种形式可用:CGMutablePathRef和UIBezierPath,均可以通过制定控制点数组的形式唯一确定曲线,也可以通过矩形内切椭圆唯一确定曲线。下面是两者的例子:

7.1 CGMutablePathRef

通过 关键点曲线连接 唯一确定

    // 贝塞尔曲线关键帧
    // 设置路径, 绘制贝塞尔曲线
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, 200, 200); // 起始点
    CGPathAddCurveToPoint(path, NULL, 100, 300, 300, 500, 200, 600);
    // CGPathAddCurveToPoint(path, NULL, 控制点1.x, 控制点1.y, 控制点2.x, 控制点2.y, 终点.x, 终点.y);
    // 设置path属性
    keyframeAnimation.path = path;
    CGPathRelease(path);

    // 设置其他属性
    keyframeAnimation.duration = 4;
    keyframeAnimation.beginTime = CACurrentMediaTime() + 1; // 设置延迟2秒执行, 不设置这个属性, 默认直接执行
    // 3. 添加动画到图层, 会自动执行
    [_layer addAnimation:keyframeAnimation forKey:@"GGKeyframeAnimation"];

7.2 UIBezierPath

通过 矩形内切椭圆 唯一确定

CAKeyframeAnimation *anima = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(SCREEN_WIDTH/2-100, SCREEN_HEIGHT/2-100, 200, 200)];
    anima.path = path.CGPath;
    anima.duration = 2.0f;
    [_demoView.layer addAnimation:anima forKey:@"pathAnimation"];

通过 关键点直线连接 唯一确定

-(void)pathAnimation2{

    //创建path
    UIBezierPath *path = [UIBezierPath bezierPath];
    //设置线宽
    path.lineWidth = 3;
    //线条拐角
    path.lineCapStyle = kCGLineCapRound;
    //终点处理
    path.lineJoinStyle = kCGLineJoinRound;
    //多条直线
    [path moveToPoint:(CGPoint){0, SCREEN_HEIGHT/2-50}];
    [path addLineToPoint:(CGPoint){SCREEN_WIDTH/3, SCREEN_HEIGHT/2-50}];
    [path addLineToPoint:(CGPoint){SCREEN_WIDTH/3, SCREEN_HEIGHT/2+50}];
    [path addLineToPoint:(CGPoint){SCREEN_WIDTH*2/3, SCREEN_HEIGHT/2+50}];
    [path addLineToPoint:(CGPoint){SCREEN_WIDTH*2/3, SCREEN_HEIGHT/2-50}];
    [path addLineToPoint:(CGPoint){SCREEN_WIDTH, SCREEN_HEIGHT/2-50}];
//    [path closePath];

    CAKeyframeAnimation *anima = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    anima.path = path.CGPath;
    anima.duration = 2.0f;
    anima.fillMode = kCAFillModeForwards;
    anima.removedOnCompletion = NO;
    anima.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    [_demoView.layer addAnimation:anima forKey:@"pathAnimation"];
}
2016-04-28 11:26:45 msyqmsyq 阅读数 1339

常用的3D动画类型同仿射变化一样有旋转平移缩放,如下:

 CATransform3DMakeScale(0.5, 0.5, 1.0);  //x,y,z放大缩小倍数

 CATransform3DMakeRotation(1.57, 1, 1, 0); //1.57表示所转角度的弧度 = 90Pi/180 = 90*3.14/180

CATransform3DMakeTranslation(0, 0, 0); //位置移动

我们想给tableview在cell即将出现的时间添加动画,用到的tableview代理方法如下:

-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;


//添加每个cell出现时的3D动画

-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{

    

    CATransform3D rotation;//3D旋转初始化对象

    rotation = CATransform3DMakeRotation( (90.0*M_PI)/180, 0.0, 0.7, 0.4);//角度控制

    

    //逆时针旋转

    rotation.m34 = 1.0/ -600;

    

    cell.layer.shadowColor = [[UIColor blackColor]CGColor];

    cell.layer.shadowOffset = CGSizeMake(10, 10);

    cell.alpha = 0;

    

    cell.layer.transform = rotation;

    

    [UIView beginAnimations:@"rotation"context:NULL];

    //旋转时间

    [UIView setAnimationDuration:0.8];

    cell.layer.transform = CATransform3DIdentity;

    cell.alpha = 1;

    cell.layer.shadowOffset = CGSizeMake(0, 0);

    [UIView commitAnimations];

}

另外一种写法稍有不同,UIView动画改用block来进行简化,


最后上一张效果图:






2018-05-14 16:22:44 u013282174 阅读数 1366

今次我们来弄一个好玩的效果,纯粹是好玩,我觉得这个效果应该很少在实际项目中用得到吧(当然不排除一些变态项目)。

图1

当然这个效果运用的核心技术就是CoreAnimation的CATransform3D(还有我的DHVector2D!),那么CATransform3D是个啥玩意呢?不要着急,我们先来了解一点关于计算机图形学的,非常非常基础的东西:矩阵变换和齐次坐标。

计算机图形学是干啥的呢,简单来说的话,因为咱们的电脑(手机)屏幕是二维的,或者说像素点是二维的,但是我们往往要去显示三维的内容(就像上图那样的非常自然的3D变换),那么我应该如何处理这些像素点让它们显示出来的东西看起来是三维的呢?或者说,如何把3维的内容投射到二维平面上去?这就是计算机图形学研究的大致方向,通常一家游戏公司招聘的时候,最首要的要求就是掌握计算机图形学。而计算机图形学的基础是线性代数,怎么样,后悔大学没好好学线性代数了吧?

那么这篇博客我会以尽量简短的内容帮大家理解iOS中的CATransform3D是如何帮助系统让平面的图像(比如一个普通的CALayer)能够进行看起来是三维的变形的,如果你有线性代数的基础,那么在讲解具体代码之前的那部分关于计算机图形学的内容你读起来应该会比较轻松,如果你没有学过线性代数,那么希望你至少能明白一个二维向量是什么东西,那样也能尽量看明白前面的内容,否则的话,只能直接去代码实现的部分了。当然,您也可以参考我技巧篇的这篇博客来对二维向量有一个大致的理解。

二维图像的显示

在讲CAShapeLayer那一章的时候提到过位图和矢量图,而我们的显示器就是一张大大的位图。显示器通过密集地排布像素点来显示任意的图像。那么既然硬件上是这么玩的,我们的UIView和CALayer在显示的时候肯定也是通过排布像素点来绘制图像的。

绘制信息会通过CPU进行计算,在垂直同步信号(iOS默认是开启垂直同步的,这时屏幕刷新信号就等同于垂直同步信号)到来时再提交给GPU进行渲染,然后GPU渲染好了以后提交给帧缓冲区,最后由显示器从帧缓冲区中获得一帧图像显示到显示器上。所以GPU和屏幕都有各自的刷新率,比如你牛逼的1080,可能GPU每秒能渲染144帧图像,但是你的显示器有点辣鸡,每秒只能绘制60帧图像,那么这之间存在的差异就会导致画面撕裂,这时候就需要垂直同步技术来解决画面撕裂。具体的大家可以去网上搜索学习,这里就不做过多解释了。

总之我们要知道一个视图或者CALayer也是通过像素点来显示的,这些像素点的信息由CPU先计算好然后在屏幕刷新信号到来时提交给GPU,然后按照上面的流程显示到屏幕上。

矩阵变换

学习过线性变换的朋友知道,对一个向量进行线性变换(比如旋转、拉伸、斜切等)只需要乘以一个对应的变换矩阵即可。那么为什么乘以这个矩阵就可以得到变换后的向量?以及这个矩阵是怎么得来的?矩阵变换的这部分内容(基向量、线性变换、线性变换的复合、平移变换)主要解决这两个问题,如果感兴趣的话可以读一读,只需要你了解二维向量的姿势即可。如果不感兴趣当然可以直接跳到CATransform3D那里去。

既然我们看到的这些二维图像是通过像素点的排列显示出来的,那么计算机如何处理图像的变换(比如旋转、拉伸等)的呢?

图像的显示涉及到像素点,每个像素点有两个属性:坐标和颜色。在图像进行变形的时候,每个像素点对应的颜色是不会改变的,但是大家应该很轻易的能想到,它们的坐标在变化。那么坐标是如何变化的呢?

作为开发者,我们当然希望存在这样一个神奇函数:它接收一个点为输入参数,然后返回一个新的点,这个返回的点就是输入点变换后所在的点。这样我们把图像上所有的点都拿去调这个函数,就可以得到变换后的图像的所有点的坐标了,那么变换后的图像自然就能画出来了。

这个函数存在吗?实际上在线性代数中有一个和它极其相似的函数,其实在线性代数中它不叫函数,叫变换:线性变换。当然你完全可以把它当做函数来看,因为它也是接收参数,返回结果。线性变换处理的对象是向量,它接收一个向量作为输入参数,然后返回变换后的向量,向量和点之间又存在着那么一丝丝微妙的关系,我们当然可以用线性变换作为入口来考虑上面提到的神奇函数。

所以我们先来看点线代和图形学的基础姿势。

基向量

在平面系统中,我们可以定义一对基向量,用它们来表示平面中任意的向量。如何表示呢?通常在平面坐标系中我们取两个坐标轴上的单位向量为基向量(方向沿坐标轴正方向、长度为1):x轴上的基向量,记作i=(1,0),y轴上的基向量,记作j=(0,1)

定义了基向量以后,该系统中任意的向量都能用它们的线性组合(加法和数量积)来表示。也就是考虑任意一个向量v=(x,y),那么根据向量加法,我们可以构造两个新向量

a=(x,0),b=(0,y)v=a+b
而根据向量数量积,有
a=xi,b=yj
所以有

v=a+b=xi+yj

所以任意的向量都可以表示为基向量的线性组合,这是非常重要的思想,请牢牢记在脑海中。

实际上基向量我们可以任意去取,你可以在坐标系中任意找两个向量作为基向量,只要这一对基向量线性无关(不共线),那么它们就可以用来表示这个坐标系下所有存在的向量。

我们来简单证明一下:比如我们自己随意取的基向量分别是i=(a,b),j=(c,d),那么任意向量v=(x,y)如何表示呢?同样因为要用基向量的线性组合来表示,我们只考虑加法和数乘。则对于任意的向量v=(x,y),一定存在两个实数nm,使得

v=ni+mj=(na,nb)+(mc,md)=(na+mc,nb+md)=(x,y)

那么就有方程组:

{x=na+mcy=nb+md

因为ij线性无关,即abcd,也就是adbc,那么方程就存在唯一解:

n=dxcyadbc,m=aybxadbc

那么对于任意的向量v=(x,y),和一对基向量i=(a,b),j=(c,d)我们都能找到两个常数n,m来表示这个向量

v=ni+mj=dxcyadbci+aybxadbcj

顺便一提,如果这对基向量线性相关(共线),那么它们就只能表示它们所在的那条直线上的所有向量;而如果它们都是零向量,则它们只能表示零向量。这应该比较好理解。

关于基向量,要记住两点,非常重要:

你可以取平面上任意一对向量作为基向量

如果这对基向量线性无关,则它们可以表示平面上所有向量;如果线性相关,则它们只能表示它们所在的直线上的所有向量;如果它们都为零向量,则它们只能表示零向量。

这对基向量能表示的所有的向量的集合,叫做这对基向量张成的空间。在平面中,如果这对基向量线性无关,则它们张成的空间就是整个平面;如果它们线性相关,则张成的空间就是它们所在的直线;如果它们是零向量,则它们张成的空间就是原点。

上面的结论同样适用于三维空间甚至更高维空间,大家可以尝试自己去推广一下。

线性变换

在线性代数中有一种变换叫做线性变换,CATransform3D中直接提供的三种变换,旋转(rotate)、拉伸(scale)都是线性变换,而平移(translation)则不是线性变换!后面会专门针对平移变换进行讨论。

线性变换是什么呢,当然线性代数书上给出了明确的定义,而我们作为IT人员,我觉得用一个programmer的思维方式来描述它更为合适。

所以现在我们来以programmer的思维来理解线性变换,考虑有一个函数f,这个函数接收一个向量作为输入参数,然后产生一个新的向量作为返回值,我们先来写一段f的伪代码:

vector f(vector v) {
    vector x = v进行某些计算后的结果
    return x;
}

就相当于,我们有一个向量v,然后把v作为参数传入f,然后通过其返回值得到一个新的向量x

x=f(v)

那么函数f就是一个变换。所以变换就是把一个向量映射成另一个向量的过程(比如旋转,一个向量旋转后就变成另一个向量了)。因为这个映射的过程可以是任意的,如果这个映射的过程满足某些条件的话,嘿嘿,那么这个变换就可以叫做线性变换了。

ok,这“某些条件”是哪些条件啊?按照书上对线性的定义,函数需要满足:

f(v+u)=f(v)+f(u)

f(cv)=cf(v)

则该函数所代表的变换就是线性的,即可加性和等比例(一阶齐次)。

注意上面两个式子中的向量指的任意向量,c指的任意常数。

而具体地,线性变换如何操作?举个例子,我要实现一个旋转的线性变换,应该对输入向量进行怎样的算法?

还记得我们的基向量吗,这里我不知道数学家们当时的脑回路是怎样的,我只能说这波操作极其风骚。

接下来会出现很多向量相关的等式,但是都是非常基础简单的,不要被吓到,勇敢的去读!(如果比较难理解,大家可以先把下面提到的所有“线性变换”暂时理解成“把一个向量沿原点进行旋转”)

我们知道,平面上任意一个向量都能用基向量的线性组合来表示。我们首先定义一对基向量:

i=(1,0),j=(0,1)

如果有任意线性变换L,将它作用于任意向量v=(x,y),即L(v),等同于L(xi+yj)

由线性变换的性质:

L(v)=L(xi+yj)=L(xi)+L(yj)=xL(i)+yL(j)

我们来关注一下结论:

L(v)=xL(i)+yL(j)

大家看着这个等式,能想到什么呢?我再放一个等式在这大家再对比看一下:

v=xi+yj

还看不出来?再进一步,我们定义两个新的基向量及输出向量:

u=L(v),k=L(i),l=L(j)

则有:

(1)u=xk+yl

(2)v=xi+yj

这样就非常清晰了:对平面上任意一个向量进行线性变换,相当于把平面上的基向量进行该线性变换,然后用新的基向量来表示变换后的向量。也就是对于任意向量v=(x,y),对它进行线性变换L,就相当于对基向量先进行线性变换L,得到新的基向量k,l,再用这对新的基向量表示向量xk+yl就是向量v进行变换后的向量了。

接下来我们对(1)式进行变形:

(3)L(v)=xk+yl=(x,y)(kl)

其中k=L(i),l=L(j)

若经过线性变换后得到新的基向量为k=(a,b),l=(c,d),那么我们可以构造一个矩阵At=(kl)=(abcd)

对(3)式进一步计算:

L(v)=xk+yl=(x,y)(kl)=(x,y)(abcd)=(x,y)At=vAt

只关注一下结果:

L(v)=vAtAt=(kl)=(abcd)

看看我们得到了什么结论:

对向量进行线性变换实际上就是让向量乘以一个矩阵At,所以最关键的过程就是找到这个矩阵,然后让我们的输入向量乘以该矩阵,就完事了。

这样我们就解决了“为什么线性变换就是向量乘以一个矩阵”的问题,接下来我们来看如何构造这个矩阵。

我们先来把伪代码写一写:

vector linear_transformation(vector v) {
    // 找到该变换对应的矩阵
    matrix At = (a,b,c,d);
    // 输入向量左乘矩阵A
    vector u = vAt;

    return u;
}

这里我们把矩阵At叫做变换矩阵。

具体地,变换矩阵At如何来找呢?举个例子,如果我们的线性变换要让所有输入向量都逆时针旋转θ°,按照我们上面基向量的思想,对于任意的输入向量v=(x,y)我们只需让两个基向量i=(1,0)j=(0,1)先逆时针旋转θ°,然后得到两个新的基向量k=(a,b)l=(c,d),然后得到变换矩阵A=(abcd),接下来就是如何求a,b,c,d了。先画个图,如图:

图2

在平面直角坐标系中,红色的两个向量为我们的基向量ij,蓝色的一对向量为基向量逆时针旋转θ°后得到的新的基向量kj,因为其长度均为1,那么可以得到

k=(cosθ,sinθ)l=(sinθ,cosθ)

所以我们得到变换矩阵

A=(cosθsinθsinθcosθ)

注意顺时针旋转的情况有所不同,大家可以自己去求一下顺时针旋转的变换矩阵。

那么对于任意输入向量v=(x,y),其进行线性变换(这里是逆时针旋转θ°)后的输出向量就是

u=L(v)=vAt=(x,y)(cosθsinθsinθcosθ)=(xcosθysinθ,xsinθ+ycosθ)

这个就是向量的旋转公式了,并且我们还解释了为什么任意向量右乘变换矩阵就能得到变换后的新向量(变换矩阵就是由新的基向量构成的,右乘该矩阵的结果刚好等于用这对新的基向量的线性组合来表示输出向量的结果,见(1)式),并且我们还知道了如何构造变换矩阵(就是求变换后的新的基向量的终点的值是多少,画个图出来就很好理解了)。

大家可以自己尝试求一下顺时针旋转和缩放的变换矩阵。

线性变换的复合

如果我一个向量要作多次变换呢?比如我先旋转,再拉伸要如何操作呢?

那不管怎样,我们先构造一个旋转矩阵Arotate,一个拉伸矩阵Ascale。对于任意的输入向量v,先旋转,那么得到旋转后的输出向量u=vArotate,然后把u作为拉伸的输入向量,进行拉伸,得到拉伸后的输出向量w=uAscale。也就是说,最先旋转再拉伸的输出向量为:

w=uAscale=(vArotate)Ascale

而线性运算乘法满足结合律,所以有

w=(vArotate)Ascale=v(ArotateAscale)=vAt

其中

At=ArotateAscale

也就是说,我们可以将多个变换矩阵复合成一个矩阵,然后输入向量乘以(右乘)这个复合矩阵就相当于进行了多个变换。注意先后顺序,先变换的矩阵要放在乘法的最左边,然后依次右乘接下来的变换矩阵。

平移变换

为什么平移变换不是线性变换呢?在几何上,向量的线性变换的一个必要条件是:线性变换作用于坐标系上所有的点,变换后原点的位置不会发生改变。显然,平移变换后原点的位置会随着平移而发生改变,从几何的角度平移变换也不是线性变换。

顺便一提,另一个必要条件是:变换前处于同一直线上的点,变换后仍要处于同一直线,且变换后它们之间的距离之比与变换前相同(比如变换之前处于同一直线上的三个点A,B,C,变换后分别对应A’,B’,C’,那么A’,B’,C’必须也处于同一直线,且AB/BC = A’B’/B’C’)。你也可以这样理解:变换前所有处于同一直线且等距的点,在变换后也处于同一直线且等距。

我们在齐次坐标中再来具体讨论平移变换如何实现。

齐次坐标

如果大家按住cmd点进CATransform3D的定义里面去就可以看到,CATransform3D实际上是一个四阶方阵(4x4矩阵)。

图3

备注写着Homogeneous three-dimensional transforms,意思就是齐次3D变换。

为什么要用四维的矩阵表示三维的内容呢,这是使用了齐次坐标,1是数学家们发明出来用来解决欧式几何无法解决的透视问题,在欧式几何(笛卡尔坐标系)中,两条平行线永远不会相交,但是在透视空间中,两条平行线是可以相交于无穷远,如图,火车轨道的两边相汇于无穷远处。2是用来区分点和向量之间的区别。

图4

齐次坐标下的点和向量的区别

我们先从坐标点和向量坐标表示的区别开始。我们知道平面上一个点可以用一个二元元组来表示:P(x,y),而一个向量也可以如此表示:v=(x,y),这有啥区别呢?

点的坐标是相对于原点的,而向量的坐标是向量终点相对于向量起点的,如果一个向量的起点就是坐标原点,那么此时向量和点在线性变换时没有任何区别。

既然点坐标是相对于原点的,我们可以把点P(x,y)看做是原点O(0,0)沿着向量v=(x,y)平移后的结果。

那么对于平面上的一对基向量i=(1,0),j=(0,1),我们可以这样表示向量:

(1)v=xi+yj

而点则看做原点沿着向量的平移,有:

PO=v

则可以表示点P

(2)P(x,y)=xi+yj+O

把(1)式和(2)式写成向量相乘的形式:

v=(x,y)(ij)=(x,y,0)(ijO)

P=(x,y,1)(ijO)

我们看到,一旦我们把原点O作为新的基向量(零向量)来考虑,点和向量有了不同的表示形式。多出的这一维的数字表示和坐标原点的关系,向量是0,表示向量和坐标系原点没有关系,无论你原点在哪都不影响我向量的大小和方向;点是1,表示点坐标是和坐标系原点密切相关的,不同坐标系原点下的P(x,y)可能会画出不同的点来。

像这样用N+1维元组来表示N维的点和向量就是齐次坐标表示。

齐次坐标下的平移变换

在此基础上我们再来看平移变换。在线性变换中,点和起点在原点的向量之间是可以直接互相转换的,比如你要让一个点绕着坐标原点旋转,那就可以让一个表达式和点相同的向量绕坐标轴原点旋转(乘以旋转矩阵),然后转换成点即可,其实就是让点(x,y)乘以旋转矩阵就完事了。但是你没法通过乘以某个矩阵让点进行平移:

比如对于平面上任意一点P(x,y),要让它沿着向量v=(a,b)进行平移,那么平移后的点就是P(x+a,y+b)。现在我们来尝试找一个变换矩阵At,使得:

(x,y)At=(x+a,y+b)

你会发现永远也找不到这样一个矩阵。现在我们引入齐次坐标,也就是把点用P(x,y,1)来表示,平移后的点就是P(x+a,y+b,1),现在再来尝试找一个变换矩阵At,使得:

(x,y,1)At=(x+a,y+b,1)

我们就可以找到一个3x3矩阵At=(100010ab1)。显然,为了适配平移变换的问题,我们的旋转和缩放等线性变换也应该放到齐次坐标下来。在二维平面上,对应的旋转矩阵(逆时针旋转)和缩放矩阵的齐次坐标表示分别为:

Arotate=(cossin0sincos0001),Ascale=(sx000sy0001)

这样在进行变换的复合时,就可以加上平移矩阵了。

这里顺便提一下,图像变换的顺序一定要是先缩放、再旋转、最后平移。这个原因涉及到物体坐标系到世界坐标系的转换,缩放既不改变坐标原点,也不改变坐标轴方向;旋转不改变坐标原点,但会影响坐标轴方向;平移则干脆连坐标原点都变了,所以变换的复合必须按着这个顺序来,不然就会出现奇怪的效果。

这样对于平面上任意的点P(x,y),其在齐次坐标下的表示为P(x,y,1),然后对它实施各种变换,进行变换后的新点为P(x,y,1),然后转换回笛卡尔坐标:P(x,y),这样齐次坐标产生的新的维度就不影响我们在笛卡尔坐标系下的点的表示。

你可能会发现,如果让向量来平移,就算是齐次坐标也无法实现,因为我们找不到这样一个变换矩阵At,使得

(x,y,0)At=(x+a,y+b,0)

这也印证了我们之前的结论,向量只有方向和大小,没有位置的概念,所以平移变换对一个向量而言是没有意义的。

同时我们可以验证平移变换的可加性,比如考虑一个沿着v=(a,b)平移的平移变换:

f(x,y)=(x+a,y+b)

我们验证可加性,对于任意两个点U(ux,uy)V(vx,vy),有

f(U+V)=f(ux+vx,uy+vy)=(ux+vx+a,uy+vy+b)

f(U)+f(V)=f(ux,uy)+f(vx,vy)=(ux+a,uy+b)+(vx+a,vy+b)=(ux+vx+2a,uy+vy+2b)

所以

f(U+V)f(U)+f(V)

所以平移变换不满足可加性,从线性变换的定义出发也验证了平移变换不是线性变换。

以上就是齐次坐标的第一个作用:解决向量和点之间的区别。

向量没有位置的概念,而点有。而旋转和缩放对于位置是没有关系的,如图,无论你把向量放在哪里,只要向量的表达式不变,那么它旋转后的结果也不变,缩放同理。

图5

那么对于旋转和缩放,点和向量之间的差别(位置)就不起作用了,那么对于这两个变换,点和向量是可以完美互相转换的(相当于对于旋转和缩放而言,它们认为向量和点是同一个东西)。但是我们的变换需求还有平移变换,而平移变换对于没有位置概念的向量而言是没有意义的(N维向量找不到N维平移变换矩阵,也就是用坐标值乘以矩阵来实现变换对于平移变换而言就不存在了),为了适配缩放和旋转(使用矩阵乘法来表示一个变换,这样才能实现变换的复合,将多个变换表示为一个矩阵),我们把N维的点和向量用N+1维的形式来表示,这样对于新的表示下的点P(x,y,1),就找到了能够用矩阵乘法来表示的平移矩阵,解决了适配的问题。

新的这一维的值如果是1,则表示一个点(因为它和坐标原点相关),如果是0,则表示一个向量(和坐标原点无关),或者表示一个无穷远处的点(无穷远处的点无论你坐标原点在哪,它还是无穷远处的点,所以也和坐标原点无关)。

齐次坐标下的平行线相交问题

最后我们来看,齐次坐标解决透视空间下的两条平行线可以相交的问题。

我们回到齐次坐标对点的表达式P(x,y,1),不知道有没有朋友在考虑这样一个问题:多的那一位表示和坐标原点“有”关系或者“无”关系,那不就是“非空”与“空”么,也就是“非零”与“零”的关系,那既然有关系用1来表示,为什么不可以用2、3、4…来表示呢?

当然数学家们也想到了这个不严谨的地方,所以他们添加了一条定义:

即当k非零时,所有形如(kx,ky,k)的三元组都表示同一个点,比如(x,y,1)(2x,2y,2)就表示同一个点。由此我们就可以引出齐次坐标的定义,即给定一个二维点(x,y),那么形如(kx,ky,k)的所有三元组就都是等价的,它们就是这个点的齐次坐标。对每一个齐次坐标,我们只要把它除以三元组中的第三个数,即可得到原始的二维点坐标。这也就是为什么这玩意叫做“齐次”坐标。

而当k=0时,因为除数不能为0,也就是点(x,y,0)是没有意义的,毕竟无论坐标原点在哪,你也无法表示它,这样的点当然就在无穷远的地方了。

好,现在我们开始来解决平行线的相交问题,考虑两条直线:

{Ax+By+C=0Ax+By+D=0

这两条直线是线性相关的(斜率一样,所以是两条平行线)。

在笛卡尔坐标中,如果CD,那么方程组无解;如果C=D那它们就是同一条直线了。

现在我们令kx=x,ky=y,放到透视空间下来求解:

{Axk+Byk+C=0Axk+Byk+D=0

整理一下得到

{Ax+By+Ck=0Ax+By+Dk=0

现在我们在CD的情况下得到一组解(x,y,0),也就是说这两条直线相交于无穷远处的一点(x,y,0)。所以3D图像投射到平面上时,就需要使用齐次坐标来表示点在透视空间下的表示(比如上面的火车轨道那张图,就是平面图形显示3D内容时,如何表示两条平行线的),我们在接下来的CATransform3D就可以看到。

CATransform3D

在开始CATransform3D之前,我们先来回顾一下上面得到的一些结论:

  1. 图像是由像素点构成的,要实现图像的各种变形变换,需要一个变换函数,将一个点作为输入参数,输出变换后的点。把构成该图像的所有像素点都拿去调用这个函数,就能实现图像的变形了;
  2. 在线性代数中有一种变换叫线性变换,它接收一个向量作为输入参数,输出变形后的向量,比如一个逆时针旋转90°的线性变换,接收任何一个向量,输出的向量就是原向量逆时针旋转90°后的向量;
  3. 线性变换的过程实际上就是输入向量乘以某个变换矩阵。我们需要实现的三种基本变换:平移、缩放、旋转中,缩放和旋转是线性变换(满足可加性和一阶齐次),由于矩阵乘法拥有结合律,所以多个线性变换可以通过它们的变换矩阵相乘复合成一个变换矩阵,比如我们可以用缩放矩阵乘以旋转矩阵,得到的结果就是一个描述先缩放再旋转的变换矩阵,输入向量乘以这个矩阵,输出的向量就是先缩放后旋转的结果;
  4. 对于平移变换,为了让它也能通过矩阵乘法进行变换的复合,我们发现只有在齐次坐标下才能找到这样一个矩阵,而为了能让平移变换也加入旋转和缩放的复合运算中,旋转和缩放也应该在齐次坐标下来表示,这样它们才能相乘。

有一点要说明一下,以免有同学钻进了牛角尖出不来。并不是因为旋转和缩放是线性变换所以它们才用矩阵乘法来表示。我们描述一个变换,实际上是对向量进行操作,只要输出满足我们的效果就可以了。而之所以要用矩阵乘法来表示旋转和缩放,是因为我们恰好能很方便的通过基向量来找到这样的变换矩阵,并且矩阵乘法满足结合律,多个变换就可以用一个矩阵来表示。平移变换不是线性变换,但是它仍是一个变换,只要是变换,就是考虑输入向量通过某些操作得到输出向量。既然旋转和缩放是用的矩阵乘法,那么为了让平移也能复合进去,我们应该优先考虑平移变换也用矩阵乘法来实现。很明显平移变换最简单的实现就是f(x,y)=(x+a,y+b),这样表示一个点(x,y)向x轴方向平移a个单位,向y轴方向平移b个单位。输入为P(x,y),输出为P(x+a,y+b),这样f就是一个完美的平移变换函数。但是如果这样来表示平移变换,就没办法通过矩阵乘法与缩放和旋转进行复合了。所以我们在找这个平移矩阵时,发现只有在齐次坐标下,这个矩阵才存在,于是为了让平移变换用矩阵乘法表示,点和变换矩阵都应该在齐次坐标下,这时旋转和缩放也就应该用齐次坐标来表示了。而线性变换和非线性变换的变换矩阵的区别在于,线性变换的变换矩阵是通过基向量变换后的结果来合成的,因此很好找,而非线性变换,比如平移变换,是“硬算”出来的,它和“用变换后的基向量来表示变换后的向量”毫无关系。再仔细看看基向量那一部分,我们是通过线性变换才有的性质(可加性和一阶齐次)才得出了“用变换后的基向量来表示变换后的向量”的结论,非线性变换可不能这么做。

基于CATransform3D的变换矩阵

从矩阵变换到齐次坐标,为了方便大家理解,我是以二维的情况进行的各种推导,这些结论都可以推广到高维上去。

比如我们接下来就要看的三维变换CATransform3D。在齐次坐标开始的时候我就截了个图,这玩意是一个矩阵,没错,它是一个施加于CALayer的变换矩阵。也就是CALayer上所有的像素点,最终都会乘以这个变换矩阵来实现各种奇奇怪怪的变换效果,比如平移缩放旋转。可能大家都知道,如果要让一个layer进行缩放的变形,一般是这样写的:

// 让一个layer在x轴方向拉伸2倍,在y轴方向拉伸3倍,在z轴方向拉伸1倍。
// 当然一般的layer是没有厚度的概念的,所以z轴的拉伸对layer而言就是没有意义的
layer.transform = CATransform3DMakeScale(2, 3, 1);

这就是改变layer的transform,也就是改变layer的变换矩阵,让layer在绘制的时候,所有的像素点乘以该矩阵来实现变形。同样我们点进CATransform3DMakeScale的注释里面去看

/* Returns a transform that scales by `(sx, sy, sz)':
 * t' = [sx 0 0 0; 0 sy 0 0; 0 0 sz 0; 0 0 0 1]. */

CA_EXTERN CATransform3D CATransform3DMakeScale (CGFloat sx, CGFloat sy,
    CGFloat sz)
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

这个就是苹果为我们实现好的一个3维齐次坐标下的缩放矩阵:

t=(sx0000sy0000sz00001)

这就是三个基向量缩放后的结果组成一个矩阵再转换成齐次坐标:

xi=(1,0,0),yj=(0,1,0),zk=(0,0,1)

缩放后的基向量分别是:

si=(sx,0,0),sj=(0,sy,0),sk=(0,0,sz)

再组合成非齐次坐标下的变换矩阵:

t=(sx000sy000sz)

最后转换成齐次坐标就是我们的CATransform3DMakeScale的结果,同理大家可以看看旋转矩阵和平移矩阵:

/* Returns a transform that translates by '(tx, ty, tz)':
 * t' =  [1 0 0 0; 0 1 0 0; 0 0 1 0; tx ty tz 1]. */

CA_EXTERN CATransform3D CATransform3DMakeTranslation (CGFloat tx,
    CGFloat ty, CGFloat tz)
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

/* Returns a transform that scales by `(sx, sy, sz)':
 * t' = [sx 0 0 0; 0 sy 0 0; 0 0 sz 0; 0 0 0 1]. */

CA_EXTERN CATransform3D CATransform3DMakeScale (CGFloat sx, CGFloat sy,
    CGFloat sz)
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

/* Returns a transform that rotates by 'angle' radians about the vector
 * '(x, y, z)'. If the vector has length zero the identity transform is
 * returned. */

CA_EXTERN CATransform3D CATransform3DMakeRotation (CGFloat angle, CGFloat x,
    CGFloat y, CGFloat z)
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

由于赋值会覆盖之前的值,所以使用这三个矩阵赋值的话无论你之前经历了怎样的变换,都会被替换成这一个变换,而不是在之前的变换的基础上进行复合。当然苹果肯定为我们提供了复合的方法:

/* Translate 't' by '(tx, ty, tz)' and return the result:
 * t' = translate(tx, ty, tz) * t. */

CA_EXTERN CATransform3D CATransform3DTranslate (CATransform3D t, CGFloat tx,
    CGFloat ty, CGFloat tz)
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

/* Scale 't' by '(sx, sy, sz)' and return the result:
 * t' = scale(sx, sy, sz) * t. */

CA_EXTERN CATransform3D CATransform3DScale (CATransform3D t, CGFloat sx,
    CGFloat sy, CGFloat sz)
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

/* Rotate 't' by 'angle' radians about the vector '(x, y, z)' and return
 * the result. If the vector has zero length the behavior is undefined:
 * t' = rotation(angle, x, y, z) * t. */

CA_EXTERN CATransform3D CATransform3DRotate (CATransform3D t, CGFloat angle,
    CGFloat x, CGFloat y, CGFloat z)
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

/* Concatenate 'b' to 'a' and return the result: t' = a * b. */

CA_EXTERN CATransform3D CATransform3DConcat (CATransform3D a, CATransform3D b)
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

前三个就是在某个变换(参数t提供的变换)之上进行平移、缩放、旋转(注释就写清楚了,用t乘以新生成了平移、缩放、旋转矩阵来得到复合后的矩阵并返回)。最后一个函数就是把两个参数a和b进行乘法,也就是变换的复合。如果你要自己搞些奇怪的变换,那可以使用这个函数来帮你计算矩阵乘法。

你可以理解成:

// 这是一个数学公式而不是赋值语句
CATransform3DScale(t,sx,sy,sz) = CATransform3DConcat(t,CATransform3DMakeScale(sx,sy,sz))

顺便一提,默认的transform是

/* The identity transform: [1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1]. */

CA_EXTERN const CATransform3D CATransform3DIdentity

也就是一个单位矩阵,任何点乘以单位矩阵得到点本身,意思就是没有任何变换效果。如果你想从变形后的状态还原,那就用这个矩阵给transform属性赋值就好了。

3D旋转变换

然后我们来看我们这个效果主要用到的,旋转变换。

/* Returns a transform that rotates by 'angle' radians about the vector
 * '(x, y, z)'. If the vector has length zero the identity transform is
 * returned. */

CA_EXTERN CATransform3D CATransform3DMakeRotation (CGFloat angle, CGFloat x,
    CGFloat y, CGFloat z)
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

返回一个表示绕向量(x,y,z)旋转angle度数的transform。所以这四个参数分别表示:angle = 旋转角度(弧度)。x,y,z表示一个向量v=(x,y,z),这个向量就是旋转轴。

所以我们先来写个3D旋转的效果看看。大家应该能脑补出来,如果我们绕着z轴(垂直于手机屏幕)旋转,那么画面就是在屏幕上旋转而已,不会出现3D的效果。所以我们先来写一个绕着x轴旋转的效果试试:

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    CALayer * layer = [CALayer layer];
    layer.frame = CGRectMake(0, 0, 320, 240);
    layer.position = self.view.center;
    layer.contents = (__bridge id)[UIImage imageNamed:@"1.jpg"].CGImage;
    [self.view.layer addSublayer:layer];

    // 为了让效果更直观,我们写个动画出来看,让layer绕着x轴旋转的动画
    CABasicAnimation * animation = [CABasicAnimation animation];
    animation.keyPath = @"transform";
    animation.duration = 5;
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;
    // 绕x轴旋转π/4

    CATransform3D transform = CATransform3DMakeRotation(M_PI/4, 1, 0, 0);

    animation.toValue = [NSValue valueWithCATransform3D:transform];
    [layer addAnimation:animation forKey:@""];
}

效果如图。

图6

空间想象能力足够的同学应该能想出来,这是绕着x轴进行的旋转,不过为啥看着反而像是沿着y轴压缩了。。。不过这里我们还是通过实验得到了一个我称为左手定则的玩意,也就是旋转方向的问题:

将左手的大拇指朝向你指定给CATransform3DMakeRotation的向量的方向,另外四指自然弯曲,弯曲的方向就是旋转的方向。

带透视效果的CATransform3D旋转

上面的效果确实是绕着x轴在旋转,之所以看起来这么奇怪,是因为我们没有添加透视效果。所谓透视,就是在平面上展现空间感的一种技术。大家在自己画一个正方体的时候都知道,如果正方体的一面不是正对着画面的话,就要画成平行四边形,这样把空间中的正方形变形成了平面上的平行四边形,给人一种立体感,这就是一种透视技术。

那么CATransform3D是如何实现透视效果的呢?CATransform3D这个矩阵是一个齐次坐标表示,我们讲齐次坐标的时候就说了,齐次坐标可以用来解决透视空间下的两条平行线相交的问题,这里就不再从计算机图形学的角度通过数学和矩阵来说明了。CATransform3D的m34(第3行第4列)的元素可以用来控制透视效果,我们只需要把它设置为150011000即可:

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    CALayer * layer = [CALayer layer];
    layer.frame = CGRectMake(0, 0, 320, 240);
    layer.position = self.view.center;
    layer.contents = (__bridge id)[UIImage imageNamed:@"1.jpg"].CGImage;
    [self.view.layer addSublayer:layer];

    CABasicAnimation * animation = [CABasicAnimation animation];
    animation.keyPath = @"transform";
    animation.duration = 5;
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;

    // 设置透视变换矩阵
    CATransform3D perspectiveTransform = CATransform3DIdentity;
    perspectiveTransform.m34 = -1.f/700;
    // 将透视变换复合到旋转变换中
    // 绕x轴旋转π/4
    CATransform3D transform = CATransform3DRotate(perspectiveTransform, M_PI/4, 1, 0, 0);

    animation.toValue = [NSValue valueWithCATransform3D:transform];
    [layer addAnimation:animation forKey:@""];
}

效果如图:

图7

这样就有明显的3D变换效果了。

基于Pan手势的3D旋转控制

旋转的问题解决了,接下来我们来看如何用pan手势来控制旋转。

总体实现思路

按照我们实践篇的思路,我们仍然先分解动画效果,分解的过程是从具体到抽象,所以我们可以这样来分解:

  1. 这是一个3D旋转效果;
  2. 这个3D旋转效果是由pan手势控制的;
  3. 旋转的方向就是手指移动的方向。

这样我们一个一个来解决。第一点,3D旋转效果我们已经知道了,用CATransform3D来实现,注意用m34来控制透视效果即可。第二点,用pan手势控制旋转效果,到这里,我们所要使用的系统API就确定了:CATransform3DUIPanGestureRecognizer。第三点,关于旋转方向,3D旋转函数

CATransform3D CATransform3DRotate (CATransform3D t, CGFloat angle,
    CGFloat x, CGFloat y, CGFloat z)

中,最后三个参数x,y,z表示一个向量v=(x,y,z),旋转会以该向量为轴进行旋转。所以我们在脑海里想一想,模拟一下,如果我的手指往上滑,这个视图就是“朝上旋转”的,也就是上面我们3D旋转的那个例子的示意图的效果。而此时大家想想,这个效果的“旋转轴”是什么?我们在写代码的时候传的是(1,0,0),所以很明显这个旋转轴就是沿着x轴正方向的一个向量。也就是说我们最终要传给CATransform3D的是这个轴,而不是“旋转方向”。而旋转方向和旋转轴是有关系的:左手定则,即打开左手,大拇指指向旋转轴的方向,那么另外四指弯曲的方向就是“旋转方向”。或者我们用向量来描述的话,如果我们用D的方向来表示旋转的方向,用C来表示旋转轴的方向,那么C就是D顺时针旋转90°的结果。

旋转轴和旋转方向的问题解决了,最后一个就是手指移动的方向,这个肯定是由UIPanGestureRecognizer来提供,这样就能得到旋转方向(就是手指移动的方向),再根据旋转方向来得到旋转轴的方向,然后传给CATransform3D,就完成了一次旋转。

总结一下我们的思路:3D旋转效果用CATrasnform3D来实现;拖动手势由UIPanGestureRecognizer来实现;3D旋转方向就是手指移动的方向;手指移动的方向由UIPanGestureRecognizer提供,所以可以在回调方法里面获取到手指移动的方向;最终要传给CATransform3D的参数是旋转轴;旋转轴与手指移动方向(旋转方向)的关系是:将手指移动方向顺时针旋转90°就是旋转轴方向。

以上,我们可以通过UIPanGestureRecognizer得到手指移动方向,然后用这个方向生成一个向量,将它顺时针旋转90°得到的新向量就是我们的旋转轴向量,作为参数传给CATransform3D就行了。

通过向量计算当前手指的移动方向

接下来是最后一个难题,熟悉UIPanGestureRecognizer的同学应该知道,它的回调方法传出来的向量,即它的translationInView方法,返回的是“手指移动轨迹的起点到当前手指所在的点连成的向量”,那么我们如何来确定手指在移动的时候任意时刻手指的移动方向?我们画个图出来分析,如图

图8

假如黑色的线条就是我们手指移动的轨迹,我们在轨迹上任取一点,比如蓝色的点,当我们手指按这个轨迹移动到蓝色的点的时候,很明显,此时手指的移动方向就是蓝点的上一个点到蓝点的连线形成的方向,上一个点我用橙色来表示(为了看的更清楚橙色和蓝色我画的离得有点分开,不影响我们分析),那么橙色的箭头就是我们要找的手指的方向。然而translationInView方法返回的是当手指移动到蓝点和橙点时与起点的连线的方向,即蓝色和红色的箭头是我们已知的,我们要求出橙色箭头所代表的向量,怎么做呢?如果熟悉向量加减法的同学在这里应该能一眼就看出来了,用向量减法:

v+v=v=>v=vv

这样我们在任意时刻手指的移动方向就是:当前的translationInView减去上一次回调时传来的translationInView,我们把手势回调的代码写出来:

- (void)onPanGesture:(UIPanGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateBegan) {

    } else if (sender.state == UIGestureRecognizerStateChanged) {

    // 获取当前手指的位移(translation)
    CGPoint panTranslation = [sender translationInView:sender.view];
    // 我们要的向量是手指上次所在的点到这次所在的点连成的一个向量,这是你这次手指滑动的方向,传给transform3DRotate函数的向量是垂直于这个向量的向量。而我们已知的只有这个transition,也就是手指最开始的点到手指当前点连成的一个向量(也就是手指的位移,只考虑起始点和结束点)。
    // 画出图来就发现,我们要的向量就是当前向量-上一次手指的位移向量(向量减法)

    // 通过这个位移生成一个向量,这就是我们当前的位移向量。
    DHVector * vector = [[DHVector alloc] initWithCoordinateExpression:panTranslation];

    // 用当前的位移向量-上次的位移向量得到我们手指的位移偏移量
    DHVector * translateVector = [DHVector aVector:vector substractedByOtherVector:[self lastTranslation]];

    // 把这个向量保存起来,下次调用这个方法的时候需要拿到这次的向量,用来做减法
    // 下次再调用这个方法的时候的lastTranslation就是这次的位移向量,所以用这次的位移向量覆盖掉lastTranslation(用这次的位移向量给lastTranslation赋值)
    [self setLastTranslation:vector];

    // 随便计算一下单位旋转角度,也就是每次调用这个方法的时候应该旋转多少度(线性插值)

    CGFloat radian = 1.5f / maxTranslate_ * maxRotateRadian_;

    // 生成旋转向量,也就是要传给CATransform3DRotate函数的向量,它通过translateVector顺时针旋转90度(PI/2)得到
    DHVector * rotateVector = [DHVector vectorWithVector:translateVector];
    [rotateVector rotateClockwiselyWithRadian:M_PI/2];

    // 把旋转向量传给函数
    self.layer.transform = CATransform3DRotate(self.layer.transform, radian, rotateVector.coordinateExpression.x,  rotateVector.coordinateExpression.y, 0);


    } else if (sender.state == UIGestureRecognizerStateCancelled || sender.state == UIGestureRecognizerStateEnded) {

    }
}

那么剩下的代码就比较简单了,我们把整个获取旋转轴的过程提出来写到一个方法里面去,就可以把剩下的。注意这里涉及到了手势交互,而CALayer是不支持交互的,所以这里要用UIView来做动画。

// 手指的最大位移量和当手指达到最大位移量时对应的旋转角度,用来插值计算每次手指移动应该旋转多少度
static const CGFloat maxTranslate_ = 400.f;
static const CGFloat maxRotateRadian_   =   M_PI * 2;

@interface ViewController ()

@property (nonatomic, strong) UIView * transformView;
@property (nonatomic, strong) DHVector * lastTranslation;
@property (nonatomic, assign) CGFloat transformUnit;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor groupTableViewBackgroundColor];

    self.transformUnit = 1.5;

    [self.view addSubview:self.transformView];

    [self.view addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onPanGesture:)]];
}

- (void)setTransform3DWithPanTranslation:(CGPoint)translation
{
    // 我们要的向量是手指上次所在的点到这次所在的点连成的一个向量,这是你这次手指滑动的方向,传给transform3DRotate函数的向量是垂直于这个向量的向量。而我们已知的只有这个transition,也就是手指最开始的点到手指当前点连成的一个向量(也就是手指的位移,只考虑起始点和结束点)。
    // 画出图来就发现,我们要的向量就是当前向量-上一次手指的位移向量(向量减法)

    // 通过这个位移生成一个向量。
    DHVector * vector = [[DHVector alloc] initWithCoordinateExpression:translation];

    // 用当前的位移向量-上次的位移向量得到我们手指的位移偏移量
    DHVector * translateVector = [DHVector aVector:vector substractedByOtherVector:[self lastTranslation]];

    // 把这个向量保存起来,下次调用这个方法的时候需要拿到这次的向量,用来做减法
    [self setLastTranslation:vector];

    // 随便计算一下单位旋转角度,也就是每次调用这个方法的时候应该旋转多少度

    CGFloat radian = self.transformUnit / maxTranslate_ * maxRotateRadian_;

    // 生成旋转向量,也就是要传给CATransform3DRotate函数的向量,它通过translateVector顺时针旋转90度(PI/2)得到
    DHVector * rotateVector = [DHVector vectorWithVector:translateVector];
    [rotateVector rotateClockwiselyWithRadian:M_PI/2];

    // 把旋转向量传给函数
    self.transformView.layer.transform = CATransform3DRotate(self.transformView.layer.transform, radian, rotateVector.coordinateExpression.x,  rotateVector.coordinateExpression.y, 0);
}

#pragma mark - callback
- (void)onPanGesture:(UIPanGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateBegan) {

    } else if (sender.state == UIGestureRecognizerStateChanged) {

        [self setTransform3DWithPanTranslation:[sender translationInView:sender.view]];

    } else if (sender.state == UIGestureRecognizerStateCancelled || sender.state == UIGestureRecognizerStateEnded) {

    }
}

#pragma mark - getter

- (UIView *)transformView
{
    if (!_transformView) {
        _transformView = ({

            UIView * view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 240)];
            view.transformUnit = 1.5;
            view.center = self.view.center;
            view.backgroundColor = [UIColor blueColor];
            view.layer.contents = (__bridge id)[UIImage imageNamed:@"1.jpg"].CGImage;
            [view prepareForTransform3D];
            view;

        });

    }
    return _transformView;
}


@end

这个效果我经过简单的封装,通过category让任意视图都能通过一个方法调用来实现这一个效果,我放到了这个git仓库

总结

CoreAnimation专题的最后一篇终于结束了,整个实践篇的目的在于让大家通过我们原理篇和技巧篇的内容来解决需求中可能遇到的各种各样的动画难题,所以我在实践篇的写作中大量提及思考的过程,阅读起来可能会比较难啃,比较干涩(毕竟都是干货呢),但是我的想法是让大家读完实践篇后不仅能实现实践篇里面那么几个效果,还能拥有动画实现的基本思路(套路,即各种分解动画的思维方式),结合我们的技巧篇的各种工具,能够见招拆招,遇到什么都不怕,这样才是内力的修炼,而不是只会几个固定的招数。内力修炼的过程是比较漫长而痛苦的,我也是一步一步一个坑一个坑走过来的,希望大家都能有所收获吧!

2016-04-06 15:57:37 boring_cat 阅读数 311

先上效果图
①还未旋转之前
旋转之前
②旋转中
旋转中
③旋转后
旋转后

如图所示:
此处动画为3D效果 贴上封装好的方法(方法有二)

#import <UIKit/UIKit.h>

@interface TransView : UIView


/** 二维动画旋转*/
-(void)transition2DwithRotation:(CGFloat) transform Duration:(CGFloat)time;

/** 三维动画旋转*/
-(void)transition3DwithRotation:(CATransform3D)transform3D Duration:(CGFloat)time finishImage:(UIImage *)finishImage hadRotate:(BOOL)ret;


@end
#import "TransView.h"

@implementation TransView

-(instancetype)initWithFrame:(CGRect)frame{

    if (self = [super initWithFrame:frame]) {
        self.frame = frame;
    }

    return self;

}

/** 二维动画旋转*/
-(void)transition2DwithRotation:(CGFloat) transform Duration:(CGFloat)time{

    //让view旋转
    [UIView animateWithDuration:time animations:^{

        self.transform = CGAffineTransformMakeRotation(transform);

    } completion:^(BOOL finished) {

    }];



}

/** 三维动画旋转*/
-(void)transition3DwithRotation:(CATransform3D)transform3D Duration:(CGFloat)time finishImage:(UIImage *)finishImage hadRotate:(BOOL)ret{


    //方法一
    [UIView animateWithDuration:time animations:^{

        self.layer.transform = transform3D;

    } completion:^(BOOL finished) {


        if (finishImage) {

            self.backgroundColor = [UIColor colorWithPatternImage:finishImage];
        }

        [UIView animateWithDuration:time animations:^{
            self.layer.transform = CATransform3DMakeRotation(M_PI, 0, 1, 0);
        }];

    }];

    //方法二
//    [UIView transitionWithView:self duration:time options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{
//        if (finishImage) {
//            //默认图片
//            self.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"user_default"]];
//        }
//    } completion:^(BOOL finished) {
//        
//        if (finishImage) {
//            //旋转后的图片
//            self.backgroundColor = [UIColor colorWithPatternImage:finishImage];
//        }        
//     });
//        
//        
//    }];
}

CATransform3DMakeRotation(M_PI, 0, 1, 0);
第一个参数:以(x,y,z)的空间向量旋转到哪个角度
M_PI 3.14159265358979323846264338327950288 /* pi */
M_PI_2 1.57079632679489661923132169163975144 /* pi/2 */
M_PI_4 0.785398163397448309615660845819875721 /* pi/4 */
M_1_PI 0.318309886183790671537767526745028724 /* 1/pi */
M_2_PI 0.636619772367581343075535053490057448 /* 2/pi */

注意点:
view.transform 是二维旋转
view.layer.transform 图层可三维旋转

总之大家试试吧 谢谢你能看完!

2014-08-08 15:43:27 piaol 阅读数 138

#define kDegreesToRadian(x) (M_PI * (x) / 180.0)

 

#define kRadianToDegrees(radian) (radian*180.0)/(M_PI)

 

- (void)viewDidLoad

{

    [superviewDidLoad];

    self.title = @"测试动画";

    self.view.backgroundColor = [UIColorlightGrayColor];

    

    

    myTest1 = [[UILabelalloc]initWithFrame:CGRectMake(10, 100, 60, 40)];

    myTest1.backgroundColor = [UIColorblueColor];

    myTest1.textAlignment = NSTextAlignmentCenter;

    myTest1.text = @"张明炜";

    myTest1.textColor = [UIColorwhiteColor];

    [self.viewaddSubview:myTest1];

    

      //闪烁效果。

//    [myTest1.layer addAnimation:[self opacityForever_Animation:0.5] forKey:nil];

      ///移动的动画。

//    [myTest1.layer addAnimation:[self moveX:1.0f X:[NSNumber numberWithFloat:200.0f]] forKey:nil];

    //缩放效果。

//    [myTest1.layer addAnimation:[self scale:[NSNumber numberWithFloat:1.0f] orgin:[NSNumber numberWithFloat:3.0f] durTimes:2.0f Rep:MAXFLOAT] forKey:nil];

     //组合动画。

//    NSArray *myArray = [NSArray arrayWithObjects:[self opacityForever_Animation:0.5],[self moveX:1.0f X:[NSNumber numberWithFloat:200.0f]],[self scale:[NSNumber numberWithFloat:1.0f] orgin:[NSNumber numberWithFloat:3.0f] durTimes:2.0f Rep:MAXFLOAT], nil];

//    [myTest1.layer addAnimation:[self groupAnimation:myArray durTimes:3.0f Rep:MAXFLOAT] forKey:nil];

    //路径动画。

//    CGMutablePathRef myPah = CGPathCreateMutable();

//    CGPathMoveToPoint(myPah, nil,30, 77);

//    CGPathAddCurveToPoint(myPah, nil, 50, 50, 60, 200, 200, 200);//这里的是控制点。

//    [myTest1.layer addAnimation:[self keyframeAnimation:myPah durTimes:5 Rep:MAXFLOAT] forKey:nil];

    //旋转动画。

    [myTest1.layeraddAnimation:[selfrotation:2degree:kRadianToDegrees(90) direction:1repeatCount:MAXFLOAT] forKey:nil];

    

    

}

 

#pragma mark === 永久闪烁的动画 ======

-(CABasicACnimation *)opacityForever_Animation:(float)time

{

    CABasicAnimation *animation = [CABasicAnimationanimationWithKeyPath:@"opacity"];//必须写opacity才行。

    animation.fromValue = [NSNumbernumberWithFloat:1.0f];

    animation.toValue = [NSNumbernumberWithFloat:0.0f];//这是透明度。

    animation.autoreverses = YES;

    animation.duration = time;

    animation.repeatCount = MAXFLOAT;

    animation.removedOnCompletion = NO;

    animation.fillMode = kCAFillModeForwards;

     animation.timingFunction=[CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseIn];///没有的话是均匀的动画。

    return animation;

}

 

#pragma mark =====横向、纵向移动===========

-(CABasicAnimation *)moveX:(float)time X:(NSNumber *)x

{

    CABasicAnimation *animation = [CABasicAnimationanimationWithKeyPath:@"transform.translation.x"];///.y的话就向下移动。

    animation.toValue = x;

    animation.duration = time;

    animation.removedOnCompletion = NO;//yes的话,又返回原位置了。

    animation.repeatCount = MAXFLOAT;

    animation.fillMode = kCAFillModeForwards;

    return animation;

}

 

#pragma mark =====缩放-=============

-(CABasicAnimation *)scale:(NSNumber *)Multiple orgin:(NSNumber *)orginMultiple durTimes:(float)time Rep:(float)repertTimes

{

    CABasicAnimation *animation = [CABasicAnimationanimationWithKeyPath:@"transform.scale"];

    animation.fromValue = Multiple;

    animation.toValue = orginMultiple;

    animation.autoreverses = YES;

    animation.repeatCount = repertTimes;

    animation.duration = time;//不设置时候的话,有一个默认的缩放时间.

    animation.removedOnCompletion = NO;

    animation.fillMode = kCAFillModeForwards;

    return  animation;

}

 

#pragma mark =====组合动画-=============

-(CAAnimationGroup *)groupAnimation:(NSArray *)animationAry durTimes:(float)time Rep:(float)repeatTimes

{

    CAAnimationGroup *animation = [CAAnimationGroupanimation];

    animation.animations = animationAry;

    animation.duration = time;

    animation.removedOnCompletion = NO;

    animation.repeatCount = repeatTimes;

    animation.fillMode = kCAFillModeForwards;

    return animation;

}

 

#pragma mark =====路径动画-=============

-(CAKeyframeAnimation *)keyframeAnimation:(CGMutablePathRef)path durTimes:(float)time Rep:(float)repeatTimes

{

    CAKeyframeAnimation *animation = [CAKeyframeAnimationanimationWithKeyPath:@"position"];

    animation.path = path;

    animation.removedOnCompletion = NO;

    animation.fillMode = kCAFillModeForwards;

    animation.timingFunction = [CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseIn];

    animation.autoreverses = NO;

    animation.duration = time;

    animation.repeatCount = repeatTimes;

    return animation;

}

 

#pragma mark ====旋转动画======

-(CABasicAnimation *)rotation:(float)dur degree:(float)degree direction:(int)direction repeatCount:(int)repeatCount

{

    CATransform3D rotationTransform = CATransform3DMakeRotation(degree, 0, 0, direction);

    CABasicAnimation *animation = [CABasicAnimationanimationWithKeyPath:@"transform"];

    animation.toValue = [NSValue valueWithCATransform3D:rotationTransform];

    animation.duration  =  dur;

    animation.autoreverses = NO;

    animation.cumulative = NO;

    animation.fillMode = kCAFillModeForwards;

    animation.repeatCount = repeatCount;

    animation.delegate = self;

 

    return animation;

 

}

iOS实现3D旋转

阅读数 3343

iOS 核心动画

阅读数 3

没有更多推荐了,返回首页