Core Animation的学习 - 3

Core Animation ,学习底层绘画与动画

2016-05-18 | 阅读

仿射变换CGAffineTransform

UIViewtransform属性是一个CGAffineTransform类型,用于在二维空间做旋转,缩放和平移.CGAffineTransform是一个可以和二维空间向量做乘法的 3x2 的矩阵:

矩阵乘法,使用CGPoint的每一列和CGAffineTransform矩阵的每一行的对应元素相乘再求和,就形成了一个新的CGPoint的结果.这里的灰色元素,是为了能让矩阵做乘法,就必须要求左边的矩阵列数与右边矩阵的行数相同,所以给矩阵填充了一些标志位,使得矩阵既可以相乘,又不会改变运算结果.这些灰色的值是不需要存储的,因为其不会发生变化.

矩阵乘法,举例 :

则使用矩阵转换后,最终生成的点的坐标为 :

当图层应用变换矩阵,图层矩形中的每一个点都被响应地做变换,从而形成一个新的四边形的形状.CGAffineTransfrom中的仿射的意思是,无论矩阵使用什么值,图层中两条平行线在变换后依旧保持平行,如下图所示:

创建CGAffineTransform :

CGAffineTransform CGAffineTransformMake(CGFloat a, CGFloat b,CGFloat c, CGFloat d, CGFloat tx, CGFloat ty)

Core Graphics中提供了几个简单的变换函数:

// 旋转,以弧度为单位
CGAffineTransformMakeRotation(CGFloat angle) 
// 拉伸缩放
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
// 平移变换
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)

UIView通过设置transform属性来做变换,实际上是封装了内部的CALayer的变换调用.

CALayer同样有一个transform属性,但是类型是CATransform3D,而不是CGAffineTransform,对应UIView中的transform属性叫做affineTransfrom.

然后在详细

混合变换

Core Graphics提供一个一系列的函数,可以在一个变换的基础上做更深层次的变换.如既要缩放又要旋转的变换 :

CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)     
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)      
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

当操作一个变换时,需要一个空值,即CGAffineTransformIdentity的常量,表示不做任何转换.

最后,如果要混合两个已经存在的变换矩阵,可以直接使用以下方法:

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

上一个变换的结果会影响之后的变换,如 :

CGAffineTransform transform = CGAffineTransformIdentity; //create a new transform 
transform = CGAffineTransformScale(transform, 0.5, 0.5); //scale by 50%
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0); //rotate by 30 degrees
transform = CGAffineTransformTranslate(transform, 200, 0); //translate by 200 points
//apply transform to layer
self.layerView.layer.affineTransform = transform;

最后的平移是在旋转了30°的基础上进行平移的,平移时候的方向是向右下角进行偏移,所以转换的顺序会影响最终的结果.

倾斜变换

倾斜转换不常用,所以Core Graphics没有提供相应的函数,具体实现的话,就要自己去计算矩阵了,比较麻烦

3D变换

CG的前缀告诉我们CGAffineTransform类型属于Core Graphics框架,而这个框架是一个严格意义上的2D绘图API,并且CGAffineTransform仅仅对2D变换有效.

transform属性可以做到3D的移动或者旋转.CATransform3D也是一个矩阵,不过是一个在3维空间中做变换的4x4的矩阵:

比二维转换要复杂很多,也提供了简单的转换:

CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz) 
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

旋转的方向 :

旋转函数多了 x,y,z ,表示每个坐标轴方向上的旋转,绕Z轴的旋转相当于之前的二维空间里面的仿射旋转,但是绕X轴Y轴的旋转则突破了屏幕的二维空间,并且在用户视角中发生了倾斜.这里对于不旋转的方向设置为0,对于旋转的方向设置为1.

如下图绕Y轴旋转45度:

CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;

但是图层并没有被旋转,只是水平上压缩了一些,原因是我们在用一个斜向的视角在看它,而不是透视,所以我们要换成透视视角.

透视投影

CATransform3D的透视效果通过一个矩阵中的简单元素来控制: m34. 即矩阵中第4行第3列.m34用于按比例缩放X和Y的值来计算到底距离视角多远.

m34的默认值是0.我们可以通过设置m34-1.0 / d来应用透视效果,d代表想象中视角相机与屏幕之间的距离,以像素为单位.那应该如何计算这个举例,只能通过估算,一般500 - 1000就差不多了.然后修改一下之前代码,有:

 CATransform3D transform = CATransform3DIdentity;
//apply perspective
transform.m34 = - 1.0 / 500.0;
//rotate by 45 degrees along the Y axis
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
//apply to layer
self.layerView.layer.transform = transform;

效果如下:

灭点

当在透视角度绘图时,远离相机视角的物体会越变越小,最终缩成一个点.这个点一般是视图的中心.于是为了在应用中创建拟真效果的透视,这个点应该聚在屏幕中点,或者至少是包含所有3D对象的视图中点.

Core Animation定义了这个点位于图层的anchorPoint中.当图层进行变换时,这个灭点就是变换之前的anchorPoint的位置.

当改变一个图层的position的时候,也会改变它的灭点,做3D变换是一定要注意着一点.尽量让所有3D图层共享一个灭点.如果想要移动一个图层,应该首先将其放置在屏幕中央,然后通过平移变换来移动到指定位置,而不是之前去修改position

sublayerTransform属性

如果有多个视图或者图层,都要做3D变换,那就要分别设置相同的m34值,且共用一个灭点.

CALayer有一个属性叫做sublayerTransform,是一个CATransform3D类型,它影响到所有的子图层,这意味着可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承这个变换方法.通过这个属性,在一个容器图层设置透视变换,然后灭点也统一被设置在容器图层的anchorPoint中.这意味着,不用每次都将子图层放在屏幕重点,然后做平移,而可以随意使用positionframe来放置子图层.

但这个属性只能传递到子图层,不能传递子图层的子图层,所以如果有嵌套,还是比较麻烦的.

背面

如果将图层完全旋转180度,那么图层完全背对相机视角,这时显示的是一个镜像对称的图片. 即图层是双面绘制的,反面就是镜像图片.

但如果用户永远看不到图层的背面,那绘制背面是在浪费资源.CALayer有一个属性doubleSided,表示是否绘制图层的背面,默认是YES.

扁平化图层

对于2D情况下的旋转图片后,反方向变换,会恢复正常的状态,如下图,对图层旋转45,然后,对子图层又旋转45°返回:

但在3D情况下,就不是这个样子的了,如果让上图的内外两个视图看Y轴旋转,然后加上透视效果,最终转换的效果如下:

这个效果就很奇怪了,内部的图层确实向左旋转了,但是发生了扭曲,按道理说,它应该保持正面朝上,显示正常的方块.

这是由于Core Animation的图层虽然看上去存在于3D空间内,但是,并不都存在于一个3D空间.每个图层的3D场景确实是扁平化的.当你正面看一个图层时,子图层似乎是3D的,但是倾斜父图层时,会发现,这个3D效果只是被绘制在图层的表面而已.

这样,使用Core Animation创建非常复杂的3D场景是很困难的,不能通过图层树来创建一个3D结构的层级关系.当然,还可以使用CATransformLayer来解决这个问题.

固体对象

接下来,尝试使用3D图层布局一些知识来创建一个立方体.

- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
    //get the face view and add it to the container
    UIView *face = self.faces[index];
    [self.containerView addSubview:face];
    //center the face view within the container
    CGSize containerSize = self.containerView.bounds.size;
    face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
    // apply the transform
    face.layer.transform = transform;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up the container sublayer transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    //add cube face 1
    CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
    [self addFace:0 withTransform:transform];
    //add cube face 2
    transform = CATransform3DMakeTranslation(100, 0, 0);
    transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
    [self addFace:1 withTransform:transform];
    //add cube face 3
    transform = CATransform3DMakeTranslation(0, -100, 0);
    transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
    [self addFace:2 withTransform:transform];
    //add cube face 4
    transform = CATransform3DMakeTranslation(0, 100, 0);
    transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
    [self addFace:3 withTransform:transform];
    //add cube face 5
    transform = CATransform3DMakeTranslation(-100, 0, 0);
    transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
    [self addFace:4 withTransform:transform];
    //add cube face 6
    transform = CATransform3DMakeTranslation(0, 0, -100);
    transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
    [self addFace:5 withTransform:transform];
}

这样创建好的是只有一个正面,然后将这个整体进行旋转,以看到这个正方体的形状,但是旋转整个正方体会很麻烦,要单独针对每个面去旋转.还有一种简单的方案,是直接调整日期视图的sublayerTransform以旋转视图摄像机,即直接修改contentView图层:

perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); 
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);

这就可以看到立体的正方体了.

光亮和阴影

还想要加上阴影效果,这就需要动态地创建光线效果.为计算阴影图层的不透明度,需要得到每个面的正太向量(垂直于表面的向量),然后根据一个想象的光源计算出两个向量的叉乘结果.叉乘代表了光源与图层之间的角度,从而决定了光亮长度.

这里原文引入了GLKit框架来做向量的计算,每个面的CATransform3D都被转换成GLKMatrix4,然后通过GLKMatrix4GetMatrix3函数得出一个3×3的旋转矩阵。这个旋转矩阵指定了图层的方向,然后可以用它来得到正太向量的值。

#define LIGHT_DIRECTION 0, 1, -0.5 
#define AMBIENT_LIGHT 0.5

- (void)applyLightingToFace:(CALayer *)face
{
    //add lighting layer
    CALayer *layer = [CALayer layer];
    layer.frame = face.bounds;
    [face addSublayer:layer];
    //convert the face transform to matrix
    //(GLKMatrix4 has the same structure as CATransform3D)
    //GLKMatrix4和CATransform3D内存结构一致,但坐标类型有长度区别,所以理论上应该做一次float到CGFloat的转换
    CATransform3D transform = face.transform;
    GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform;
    GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
    //get face normal
    GLKVector3 normal = GLKVector3Make(0, 0, 1);
    normal = GLKMatrix3MultiplyVector3(matrix3, normal);
    normal = GLKVector3Normalize(normal);
    //get dot product with light direction
    GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
    float dotProduct = GLKVector3DotProduct(light, normal);
    //set lighting layer opacity
    CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;
    UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];
    layer.backgroundColor = color.CGColor;
}

加上阴影,效果如下:

在正方体中,点击事件无法正确处理,因为之前说过了,点击事件的处理有视图在父视图中的顺序决定,而不是3D空间的Z轴顺序决定.所以,只能通过设置其他视图userInteractionEnabled为NO来禁止事件传递.