• CGAffineTransform与CATransform3D


    CGAffineTransform

    1.CG的前缀告诉我们,CGAffineTransform类型属于Core Graphics框架,Core Graphics实际上是一个严格意义上的2D绘图API,并且CGAffineTransform仅仅对2D变换有效。实际上UIView的transform属性是一个CGAffineTransform类型,用于在二维空间做旋转,缩放和平移。CGAffineTransform是一个可以和二维空间向量(例如CGPoint)做乘法的3X2的矩阵(见图5.1)。

    5.1.jpeg

      UIView可以通过设置transform属性做变换,但实际上它只是封装了内部图层的变换。

    2.Core Graphics提供了一系列函数,对完全没有数学基础的开发者也能够简单地做一些变换。如下几个函数都创建了一个CGAffineTransform实例:

    1
    2
    3
    CGAffineTransformMakeRotation(CGFloat angle) 
    CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
    CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)

    旋转和缩放变换都可以很好解释--分别旋转或者缩放一个向量的值。平移变换是指每个点都移动了向量指定的x或者y值--所以如果向量代表了一个点,那它就平移了这个点的距离。

    清单5.1的例子就是使用 affineTransform对图层做了45度顺时针旋转。

    清单5.1 使用affineTransform对图层旋转45度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @interface ViewController ()
    @property (nonatomic, weak) IBOutlet UIView *layerView;
    @end
    @implementation ViewController
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        //rotate the layer 45 degrees
        CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
        self.layerView.layer.affineTransform = transform;
    }
    @end

    注意我们使用的旋转常量是M_PI_4,而不是你想象的45,因为iOS的变换函数使用弧度而不是角度作为单位。弧度用数学常量pi的倍数表示,一个pi代表180度,所以四分之一的pi就是45度。

    3.混合变换

    我们来用这些函数组合一个更加复杂的变换,先缩小50%,再旋转30度,最后向右移动200个像素:

        CGAffineTransform transform = CGAffineTransformIdentity; //scale by 50%

        transform = CGAffineTransformScale(transform, 0.5, 0.5); //rotate by 30 degrees
        transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0); //translate by 200 points
        transform = CGAffineTransformTranslate(transform, 200, 0);
        //apply transform to layer
        self.layerView.layer.affineTransform = transform;
     
    CATransform3D

    1.CALayer同样也有一个transform属性,但它的类型是CATransform3D,而不是CGAffineTransform。CALayer对应于UIView的transform属性叫做affineTransform.CATransform3D也是一个矩阵,但是和2x3的矩阵不同,CATransform3D是一个可以在3维空间内做变换的4x4的矩阵.          

    5.9.jpeg

    和CGAffineTransform矩阵类似,Core Animation提供了一系列的方法用来创建和组合CATransform3D类型的矩阵,和Core Graphics的函数类似,但是3D的平移和旋转多处了一个z参数,并且旋转函数除了angle之外多出了x,y,z三个参数,分别决定了每个坐标轴方 向上的旋转:

    1
    2
    3
    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轴比较熟悉了,分别以右和下为正方向;

    (1).绕Z轴的旋转等同于之前二维空间的仿射旋转,但是绕X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户视角看来发生了倾斜。

    (2).绕Y轴旋转图层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @implementation ViewController
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        //rotate the layer 45 degrees along the Y axis
        CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
        self.layerView.layer.transform = transform;
    }
    @end

    看起来图层并没有被旋转,而是仅仅在水平方向上的一个压缩,是哪里出了问题呢?

    其实完全没错,视图看起来更窄实际上是因为我们在用一个斜向的视角看它,而不是透视。

    (3).CATransform3D的m34元素,用来做透视

    m34的默认值是0,我们可以通过设置m34为-1.0 / d来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要,大概估算一个就好了。

    因 为视角相机实际上并不存在,所以可以根据屏幕上的显示效果自由决定它的防止的位置。通常500-1000就已经很好了,但对于特定的图层有时候更小后者更 大的值会看起来更舒服,减少距离的值会增强透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本失去透视效果,对视图应用 透视的代码见清单5.5,

    清单5.5 对变换应用透视效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @implementation ViewController
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        //create a new transform
        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;
    }
    @end

    (4).消亡点:Core Animation定义了这个点位于变换图层的anchorPoint

    (5).sublayerTransform

    CALayer有一个属性叫做sublayerTransform。它也是CATransform3D类型,但和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承了这个变换方法。

    相 较而言,通过在一个地方设置透视变换会很方便,同时它会带来另一个显著的优势:消亡点被设置在容器图层的中点,从而不需要再对子图层分别设置了。这意味着 你可以随意使用position和frame来放置子图层,而不需要把它们放置在屏幕中点,然后为了保证统一的消亡点用变换来做平移。

    //containerView 放置layerView1layerView2两个视图

    @interface ViewController ()
    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, weak) IBOutlet UIView *layerView1;
    @property (nonatomic, weak) IBOutlet UIView *layerView2;
    @end
    @implementation ViewController
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        //apply perspective transform to container
        CATransform3D perspective = CATransform3DIdentity;
        perspective.m34 = - 1.0 / 500.0;
        self.containerView.layer.sublayerTransform = perspective;
        //rotate layerView1 by 45 degrees along the Y axis
        CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
        self.layerView1.layer.transform = transform1;
        //rotate layerView2 by 45 degrees along the Y axis
        CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
        self.layerView2.layer.transform = transform2;
    }

    (6).CALayer有一个叫做doubleSided的属性来控制图层的背面是否要被绘制。这是一个BOOL类型,默认为YES,如果设置为NO,那么当图层正面从相机视角消失的时候,它将不会被绘制。

    (7).画正方体

    测试代码如下:


    - (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
    {
        //get the face view and add it to the container
        UIView *face = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 200, 200)];
        face.backgroundColor=[UIColor grayColor];
        UIButton * btn=[UIButton buttonWithType:UIButtonTypeCustom];
        [btn setFrame:CGRectMake(0, 0, 60, 25)];
        [btn setTitle:[NSString stringWithFormat:@"%ld",index+1] forState:UIControlStateNormal];
        [face addSubview:btn];
        btn.center=CGPointMake(face.frame.size.width / 2.0, face.frame.size.height / 2.0);
        [self.view addSubview:face];
        //center the face view within the container
        CGSize containerSize = self.view.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;
        
        //这就对相机(或者相对相机的整个场景,你也可以这么认为)绕Y轴旋转45度,并且绕X轴旋转45度。现在从另一个角度去观察立方体,就能看出它的真实面貌(图5.21)。
        perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
        perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
        
        self.view.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];
    }

    运行后的效果图:

    光亮和阴影

    现在它看起来更像是一个 立方体没错了,但是对每个面之间的连接还是很难分辨。Core Animation可以用3D显示图层,但是它对光线并没有概念。如果想让立方体看起来更加真实,需要自己做一个阴影效果。你可以通过改变每个面的背景颜 色或者直接用带光亮效果的图片来调整。

    如果需要动态地创建光线效果,你可以根据每个视图的方向应用不同的alpha值做出半透明的阴影图 层,但为了计算阴影图层的不透明度,你需要得到每个面的正太向量(垂直于表面的向量),然后根据一个想象的光源计算出两个向量叉乘结果。叉乘代表了光源和 图层之间的角度,从而决定了它有多大程度上的光亮。

    清单5.10实现了这样一个结果,我们用GLKit框架来做向量的计算(你需要引入 GLKit库来运行代码),每个面的CATransform3D都被转换成GLKMatrix4,然后通过GLKMatrix4GetMatrix3函数 得出一个3×3的旋转矩阵。这个旋转矩阵指定了图层的方向,然后可以用它来得到正太向量的值。

    结果如图5.22所示,试着调整LIGHT_DIRECTION和AMBIENT_LIGHT的值来切换光线效果

    清单5.10 对立方体的表面应用动态的光线效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    #import "ViewController.h" 
    #import 
    #import
    #define LIGHT_DIRECTION 0, 1, -0.5 
    #define AMBIENT_LIGHT 0.5
    @interface ViewController ()
    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
    @end
    @implementation ViewController
    - (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)
        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;
    }
    - (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;
        //apply lighting
        [self applyLightingToFace:face.layer];
    }
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        //set up the container sublayer transform
        CATransform3D perspective = CATransform3DIdentity;
        perspective.m34 = -1.0 / 500.0;
        perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
        perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 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];
    }
    @end

    5.22.jpeg

    图5.22 动态计算光线效果之后的立方体

    点击事件

    你应该能注意到现在可以在第三个表面的顶部看见按钮了,点击它,什么都没发生,为什么呢?

    这 并不是因为iOS在3D场景下正确地处理响应事件,实际上是可以做到的。问题在于视图顺序。在第三章中我们简要提到过,点击事件的处理由视图在父视图中的 顺序决定的,并不是3D空间中的Z轴顺序。当给立方体添加视图的时候,我们实际上是按照一个顺序添加,所以按照视图/图层顺序来说,4,5,6在3的前 面。

    即使我们看不见4,5,6的表面(因为被1,2,3遮住了),iOS在事件响应上仍然保持之前的顺序。当试图点击表面3上的按钮,表面4,5,6截断了点击事件(取决于点击的位置),这就和普通的2D布局在按钮上覆盖物体一样。

    你 也许认为把doubleSided设置成NO可以解决这个问题,因为它不再渲染视图后面的内容,但实际上并不起作用。因为背对相机而隐藏的视图仍然会响应 点击事件(这和通过设置hidden属性或者设置alpha为0而隐藏的视图不同,那两种方式将不会响应事件)。所以即使禁止了双面渲染仍然不能解决这个 问题(虽然由于性能问题,还是需要把它设置成NO)。

    这里有几种正确的方案:把除了表面3的其他视图userInteractionEnabled属性都设置成NO来禁止事件传递。或者简单通过代码把视图3覆盖在视图6上。无论怎样都可以点击按钮了(图5.23)。

    5.23.jpeg

    图5.23 背景视图不再阻碍按钮,我们可以点击它了

  • 相关阅读:
    在项目中运用到的导航高亮
    【转载】IE8 inlineblock容器不撑开问题(利用重绘解决)
    我的博客正式开通
    【转载】响应式网页设计的9条基本原则
    一款不错的在线SVG制作工具
    【转载】前端不为人知的一面前端冷知识集锦
    11.3 Daily Scrum
    11.11 Daily Scrum
    11.7 Daily Scrum(周末暂停两天Daily Scrum)
    11.12 Daily Scrum(保存草稿后忘了发布·····)
  • 原文地址:https://www.cnblogs.com/jingdizhiwa/p/5481072.html
Copyright © 2020-2023  润新知