IOS 3D UI:CALayer的transform扩展

转载 worldmatrix   2015-03-26 14:13  

iOS的UI是基于 UIView 类的,我们能看到的每个UI元素都是 UIView 或者 UIView 的子类。View按树形结构组织起来,树根是 UIWindow

View负责界面的交互和显示,其中显示部分由 CALayer 来完成。每个 UIView 包含一个 CALayer 实例。可以这么认为, UIView 本身是不可见的,我们能看到的都是 CALayerUIView 只是负责对 CALayer 进行管理。

UIView 的显示设置都是对 CALayer 属性的封装,但是这层封装掩盖了 CALayer 提供的3D显示功能。所以我们想让 UIView 显示3D的效果的话,需要直接操作 CALayer

要操作 CALayer 对象,首先要在工程中包含QuartzCore.framework,在文件中 #import <QuartzCore/QuartzCore.h> 头文件。QuartzCore.framework中包含了 CALayer 以及 CALayer 一些官方子类的定义。

通过设置 CALayertransform 属性,可以使 CALayer 产生3D空间内的平移、缩放、旋转等变化。

第一个例子,绕坐标轴的旋转:

原始场景如图

/media/note/2015/03/26/ios-calayer-transform/fig1.png

使用 image.layer.transform = CATransform3DMakeRotation(M_PI/6, 0, 0, 1); 绕Z轴旋转30度后的效果

/media/note/2015/03/26/ios-calayer-transform/fig2.png

使用 image.layer.transform = CATransform3DMakeRotation(M_PI/6, 0, 1, 0); 绕Y轴旋转30度后的效果

/media/note/2015/03/26/ios-calayer-transform/fig3.png

使用 image.layer.transform = CATransform3DMakeRotation(M_PI/6, 1, 0, 0); 绕X轴旋转30度后的效果

/media/note/2015/03/26/ios-calayer-transform/fig4.png

能够发现,绕Z轴的旋转比较符合预期,但是绕X轴Y轴的旋转只是在Y轴X轴上进行了一些缩放而已。这是因为,在 CALayer 的显示系统中,默认的相机使用正交投影,正交投影没有远小近大效果,所以在本例中,只能造成相应轴上的缩放。

第二个例子,透视投影

CALayer 默认使用正交投影,因此没有远小近大效果,而且没有明确的API可以使用透视投影矩阵。所幸可以通过矩阵连乘自己构造透视投影矩阵。构造透视投影矩阵的代码如下:

CATransform3D CATransform3DMakePerspective(CGPoint center, float disZ)
{
    CATransform3D transToCenter = CATransform3DMakeTranslation(-center.x, -center.y, 0);
    CATransform3D transBack = CATransform3DMakeTranslation(center.x, center.y, 0);
    CATransform3D scale = CATransform3DIdentity;
    scale.m34 = -1.0f/disZ;
    return CATransform3DConcat(CATransform3DConcat(transToCenter, scale), transBack);
}

CATransform3D CATransform3DPerspect(CATransform3D t, CGPoint center, float disZ)
{
    return CATransform3DConcat(t, CATransform3DMakePerspective(center, disZ));
}

这个函数的实现原理要参考计算机图形学的3D变换部分,以后再做解释。现在只需要了解接口的含义, center 指的是相机的位置,相机的位置是相对于要进行变换的 CALayer 来说的,原点是 CALayeranchorPoint 在整个 CALayer 的位置,例如 CALayer 的大小是(100, 200), anchorPoint 值为(0.5, 0.5),此时 anchorPoint 在整个 CALayer 中的位置就是(50, 100),正中心的位置。传入透视变换的相机位置为(0, 0),那么相机所在的位置相对于 CALayer 就是(50, 100)。如果希望相机在左上角,则需要传入(-50, -100)。 disZ 表示的是相机离 z = 0 平面(也可以理解为屏幕)的距离。

带透视效果的旋转,效果如下:

/media/note/2015/03/26/ios-calayer-transform/fig5.png
CATransform3D rotate = CATransform3DMakeRotation(M_PI/6, 1, 0, 0);
image.layer.transform = CATransform3DPerspect(rotate, CGPointMake(0, 0), 200);
/media/note/2015/03/26/ios-calayer-transform/fig6.png
CATransform3D rotate = CATransform3DMakeRotation(M_PI/6, 0, 1, 0);
image.layer.transform = CATransform3DPerspect(rotate, CGPointMake(0, 0), 200);
/media/note/2015/03/26/ios-calayer-transform/fig7.png
CATransform3D rotate = CATransform3DMakeRotation(M_PI/6, 0, 0, 1);
image.layer.transform = CATransform3DPerspect(rotate, CGPointMake(0, 0), 200);

image 的默认 anchorPoint 为(0.5, 0.5),也就是在图片中心;眼睛在图片中心点,距屏幕200单位。可以观察到,因为翻转,使图片的不同部分离屏幕距离不同,近大远小的效果使立体感大大提升。饶Z轴的旋转不因为透视产生变化,因为所有的点离屏幕距离相同,所以不会产生近大远小的透视感。

第三,更多的效果。 CALayer 的旋转和缩放是绕 anchorPoint 点的,改变 anchorPoint 的值,可以使 CALayer 绕不同的点而不只是中心点旋转缩放。在构造透视投影矩阵的例子中就可以看到, CATransform3D 可以使用 CATransform3DConcat 函数连接起来以构造更复杂的变换。通过这些方法,可以组合出更多的效果来。下面是个翻转的动画。使用 UITimer

/media/note/2015/03/26/ios-calayer-transform/fig8.png
- (void)update
{
    static float angle = 0;
    angle += 0.05f;

    CATransform3D transloate = CATransform3DMakeTranslation(0, 0, -200);
    CATransform3D rotate = CATransform3DMakeRotation(angle, 0, 1, 0);
    CATransform3D mat = CATransform3DConcat(rotate, transloate);
    image.layer.transform = CATransform3DPerspect(mat, CGPointMake(0, 0), 500);
}

最后是两个更复杂的例子。第一个是使用四张同样大小的图片围成一个框,让这个框动画旋转。

/media/note/2015/03/26/ios-calayer-transform/fig9.png
CATransform3D move = CATransform3DMakeTranslation(0, 0, 160);
CATransform3D back = CATransform3DMakeTranslation(0, 0, -160);

CATransform3D rotate0 = CATransform3DMakeRotation(-angle, 0, 1, 0);
CATransform3D rotate1 = CATransform3DMakeRotation(M_PI_2-angle, 0, 1, 0);
CATransform3D rotate2 = CATransform3DMakeRotation(M_PI_2*2-angle, 0, 1, 0);
CATransform3D rotate3 = CATransform3DMakeRotation(M_PI_2*3-angle, 0, 1, 0);

CATransform3D mat0 = CATransform3DConcat(CATransform3DConcat(move, rotate0), back);
CATransform3D mat1 = CATransform3DConcat(CATransform3DConcat(move, rotate1), back);
CATransform3D mat2 = CATransform3DConcat(CATransform3DConcat(move, rotate2), back);
CATransform3D mat3 = CATransform3DConcat(CATransform3DConcat(move, rotate3), back);

image0.layer.transform = CATransform3DPerspect(mat0, CGPointZero, 500);
image1.layer.transform = CATransform3DPerspect(mat1, CGPointZero, 500);
image2.layer.transform = CATransform3DPerspect(mat2, CGPointZero, 500);
image3.layer.transform = CATransform3DPerspect(mat3, CGPointZero, 500);

上面的例子都使用 UIImageCALayer ,但 CALayer 产生的动画可以应用在所有的 UIView 及子类上,下面是个普通界面的立体翻转效果。

/media/note/2015/03/26/ios-calayer-transform/fig10.png
float dis = 160 * 1.732f;
CATransform3D move = CATransform3DMakeTranslation(0, 0, dis);
CATransform3D back = CATransform3DMakeTranslation(0, 0, -dis);

CATransform3D rotate0 = CATransform3DMakeRotation(-angle, 0, 1, 0);
CATransform3D rotate1 = CATransform3DMakeRotation(-angle + M_PI/3.0f, 0, 1, 0);

CATransform3D mat0 = CATransform3DConcat(CATransform3DConcat(move, rotate0), back);
CATransform3D mat1 = CATransform3DConcat(CATransform3DConcat(move, rotate1), back);

view0.layer.transform = CATransform3DPerspect(mat0, CGPointZero, 500);
view1.layer.transform = CATransform3DPerspect(mat1, CGPointZero, 500);

http://www.cocoachina.com/bbs/read.php?tid-117061-page-1.html

http://www.yeolar.com/note/2015/03/26/ios-calayer-transform/

http://www.yeolar.com/note/2015/03/26/ios-calayer-transform/