Core Graphics 二: CGAffineTransform变换和CTM坐标系

Core Graphics 一: CGContext基本绘制

图形学变换需要了解矩阵
1.向量和矩阵
2.变换

变换的过程是图形的每一个点都根据一个关系变换成另一个点
在齐次坐标中,点可以被描述成矩阵
变换的过程就是点的矩阵乘上一个变换矩阵(变换关系),最后得到另给一个点矩阵

CGAffineTransform

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

CGAffineTransform提供了用于创建、连接和应用仿射转换的函数.
因为第三列总是(0,0,1),所以CGAffineTransform数据结构只包含前两列的值.
注意文档中的写法,矩阵写成一排,是从左到右从上到下的,比如一个3x3的单位矩阵,单位矩阵是对角线为1,其他全是0的矩阵,在iOS的文档中写作[1 0 0 0 1 0 0 0 1]
所以3x2的CGAffineTransform写作[a b c d tx ty]

在上面的文章中,点被描述成单列矩阵,而在Quartz中,点描述成单行矩阵因此iOS的变换矩阵和上面文章里的变换矩阵不太一样.


开头链接里的

Quartz里的

因此在Quartz中[X Y 1] [a b 0 c d 0 tx ty 1] = [aX+cY+tx bX+dY+ty 1]

综上所述,在Quartz中,变换的过程就是调用CGAffineTransform的一系列方法,生成一个CGAffineTransform结构体,这个结构体就是一个变换矩阵,然后设置UIView的UIViewGeometry分类里的transform属性,或者调用变换函数CTM,给定上下文和CGAffineTransform,来执行变换

1.CGAffineTransform的函数

  • CGAffineTransformIdentity
    变换的初始状态,行向量乘上这个矩阵什么都不会发生,也就是[ 1 0 0 1 0 0]
    CGAffineTransform transform = CGAffineTransformIdentity;
    [UIView animateWithDuration:.5 animations:^{
        [view setTransform:CGAffineTransformScale(transform, .5, .5)];
    }];

CGAffineTransform提供了几个生成特定形式变换矩阵的API

  • CGAffineTransform CGAffineTransformMakeTranslation(CGFloat tx,
    CGFloat ty)
    CGAffineTransformTranslate(CGAffineTransform t,
    CGFloat tx, CGFloat ty)
    平移变换,每个点在x轴移动tx,在y轴移动ty

  • CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
    CGAffineTransformScale(CGAffineTransform t,
    CGFloat sx, CGFloat sy)
    缩放变换,每个点的x乘上sx,y乘上sy

  • CGAffineTransformMakeRotation(CGFloat angle)
    CGAffineTransformRotate(CGAffineTransform t,
    CGFloat angle)
    旋转变换,提供一个角度,旋转变换的矩阵是[cosθ -sinθ sinθ cosθ],不过这里不需要计算矩阵,参数angle是π,
    即M_PI=180°,是顺时针计算的

  • CGAffineTransformInvert(CGAffineTransform t)
    反向变换,这个函数是对变换矩阵的一种运算,
    当t是一个缩放矩阵的时候,比如放大3倍,调用这个函数之后会变成放大1/3倍,
    当t是一个平移矩阵时,假设tx=150,ty=200,调用函数之后,tx变成-150,ty变成-200
    当t是一个旋转矩阵是,调用函数后,会变成逆时针旋转

  • CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2)
    合并两次变换,也就是将两个变换矩阵相乘,注意矩阵乘法不遵循交换率,AxB != BxA,因此变换的顺序很重要,具体参考本文开头的链接

  • CGAffineTransformEqualToTransform(CGAffineTransform t1, CGAffineTransform t2)
    对比两个变换矩阵是否相同

  • CGPointApplyAffineTransform(CGPoint point, CGAffineTransform t)
    对CGPoint使用变换

  • CGSizeApplyAffineTransform(CGSize size, CGAffineTransform t)
    对CGSize使用变换

  • CGRectApplyAffineTransform(CGRect rect, CGAffineTransform t)
    对CGRect使用变换

  • CGAffineTransformMake(CGFloat a, CGFloat b, CGFloat c, CGFloat d, CGFloat tx, CGFloat ty)
    自定义一个变换矩阵
    CGAffineTransform只提供了生成平移旋转等比缩放的函数,切变和镜像等则需要自己写CGAffineTransformMake
    CGAffineTransformMake(-1, 0, 0, 1, 0, 0); //翻转矩阵,在Quartz中是绕竖直对称轴翻转
    CGAffineTransformMake(1, 0, -.3, 1, 0, 0);//切变矩阵

2.变换的拆分与合并
变换就是矩阵,两次变换就是两个矩阵,那么,两个矩阵相乘,就是将两个变换合并起来,一次完成,
同理,复杂的变化也可以拆分
例如,一个平移矩阵[1 0 0 1 2 2] ,乘上第二个平移矩阵[1 0 0 1 2 2] 等于[1 0 0 1 4 4].
因此,上面的Translate和Scale和Rotate三个函数,都提供了两个函数,一个是需要一个CGAffineTransform,修改并返回,另一个不需要传CGAffineTransform,它会修改CGAffineTransformIdentity并返回,因此一个是叠加,一个是从初始状态计算.
当分解一个变换的时候,就需要操作同一个CGAffineTransform结构体

    DrawView *view = [[DrawView alloc]initWithFrame:CGRectMake(0, 0, 300, 400)];
    view.center = self.view.center;
    view.backgroundColor = UIColor.yellowColor;
    [self.view addSubview:view];

    CGAffineTransform transform = CGAffineTransformIdentity;
    transform = CGAffineTransformScale(transform, .5, .5);
    transform = CGAffineTransformTranslate(transform, 250, 400);
    transform = CGAffineTransformRotate(transform, M_PI/2);
    [UIView animateWithDuration:.5 animations:^{
        [view setTransform:transform];
    }];

3.变换对UIKit坐标系的影响
拆分变换需要注意坐标系的变化,当使用旋转变换后,坐标系也会发生旋转.

    CGAffineTransform transform = CGAffineTransformIdentity;
    transform = CGAffineTransformTranslate(transform, 200, -200);
    transform = CGAffineTransformRotate(transform, M_PI/2);
    transform = CGAffineTransformTranslate(transform, 200, 200);
    NSLog(@"dv=%@",NSStringFromCGRect(self.dview.frame));
    [UIView animateWithDuration:.5 animations:^{
        [view setTransform:transform];
    }completion:^(BOOL finished) {
        NSLog(@"dv=%@",NSStringFromCGRect(self.dview.frame));
    }];

在这个例子里面,view先平移,再旋转,再平移会来,其中:
1.旋转始终是以view的中心为中心进行
2.view的fram在变换前是{{362, 483}, {300, 400}},旋转90度后,frame变成{{312, 533}, {400, 300}},宽高颠倒了.
那么如果旋转45度呢,CGRec在坐标系内是一个平行于坐标轴的矩形,旋转45度就不平行了,改成M_PI_4打印一下,发现是{{264.51262658470836, 435.51262658470836}, {494.97474683058323, 494.97474683058323}},
这不太容易看出来,把宽高都改成300,再试一次,从{{362, 483}, {300, 400}}变成了{{299.86796564403573, 470.86796564403573}, {424.26406871192853, 424.26406871192853}},一个正方形旋转45度之后,对角线垂直于y轴,300x300+300x300 = 424x424,因此view的fram变成了一个包裹变换后的区域的新的矩形.
为了验证这一点,可以再创建一个viewA,始终让A的fram和View的frame相同,A也添加在VC上,给A一个透明度方便观察,然后执行动画.

 [UIView animateWithDuration:.5 animations:^{
        [view setTransform:transform];
        self.viewA.frame = self.dview.frame;
    }completion:^(BOOL finished) {
        NSLog(@"dv=%@",NSStringFromCGRect(self.dview.frame));
    }];
frame的变化.gif

3.第二次平移不是-200,200,因为坐标系的方向变了,一开始左上角是原点,朝右是x,朝下是y,旋转后,右上角是原点,朝左是y,朝下是x.
transform结构体在调用CGAffineTransform函数之后,会将transform进行修改并返回,而CGAffineTransformmake函数不需要transform参数,也就不会修改,它只是返回一个变换矩阵而已,这两种的结果是不一样的,一个是在变换的基础上继续变换,另一个是在初始状态的基础上变换.

//        transform = CGAffineTransformTranslate(transform, 200, 0);
        [UIView animateWithDuration:.5 animations:^{
//            [self.dview setTransform:transform];
            [self.dview setTransform:CGAffineTransformMakeTranslation(200, 0)];
        }];
第一次的旋转变换被丢掉了

在第一次变换的基础上第二次变换

当然,还可以合并两次变换

    transform = CGAffineTransformRotate(transform, M_PI_4);
    transform = CGAffineTransformTranslate(transform, 200, 0);
合并变换矩阵.gif

4.由于view旋转了,因此view在superView中的frame也发生了变化,origin和size可能都变了,但是view自身的坐标系变了,因此view中的subView的frame没有变,同样view的bounds是基于自身坐标系的,因此bounds的size不变.

4.CTM坐标系
Core Graphics提供了几个变换坐标系的函数,也就是CTM函数

  • void CGContextTranslateCTM(CGContextRef cg_nullable c,
    CGFloat tx, CGFloat ty)
    平移坐标系

  • void CGContextScaleCTM(CGContextRef c, CGFloat sx, CGFloat sy);
    缩放坐标系

  • void CGContextRotateCTM(CGContextRef c, CGFloat angle);
    旋转坐标系
    因为旋转的是坐标系,所以视觉效果上是以左上角(0,0)为中心进行的,这与UIView不同,angle正是顺时针,负是逆时针.

这三个函数的功能就是将绘制时使用的坐标系(CTM)进行变换,与UIKit的坐标系不同,CTM发生变化,对UIKit坐标没有影响.视图的子视图没有任何变化,frame也不会变化,但是context绘制的图形会变化.由于是坐标系的变化,所以图形的位置和大小都可能会变

///View内
- (void)drawRect:(CGRect)rect{
    if(self.shouldTransform){
//         CGContextRotateCTM(context, M_PI/8);
//        CGContextTranslateCTM(context, 400, 0);
        CGContextScaleCTM(context, .5, .5);
    }

    CGContextDrawImage(context, CGRectMake(rect.size.width/2, rect.size.height/2, 80, 80), [UIImage imageNamed:@"img"].CGImage);
}

///VC内
- (void)transform{
    self.dview.shouldTransform = YES;
    [self.dview setNeedsDisplay];
    NSLog(@"%@",NSStringFromCGRect(self.subv.frame));
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = UIColor.whiteColor;
    self.dview = [[DrawView alloc]initWithFrame:self.view.bounds];
    self.dview.backgroundColor = UIColor.lightGrayColor;
    [self.view addSubview:self.dview];
    
    self.subv = [[UIView alloc]initWithFrame:CGRectMake(300, 300, 90, 90)];
    self.subv.backgroundColor = UIColor.whiteColor;
    [self.dview addSubview:self.subv];
    NSLog(@"%@",NSStringFromCGRect(self.subv.frame));
    
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    [btn setTitle:@"transform" forState:UIControlStateNormal];
    btn.frame = CGRectMake(20, 20, 100, 40);
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(transform) forControlEvents:UIControlEventTouchUpInside];
}

上面的例子是给视图V定义一个shouldTransform属性,在VC创建一个按钮,按钮点击会设置shouldTransform为yes,然后重新绘制,并且再打印一次subV的frame
结果是subV的frame和视觉都没有变换,但是绘制在dview上的图片偏移了
在调用CTM函数前后,绘制图片的rect都是CGRectMake(rect.size.width/2, rect.size.height/2, 80, 80),因此图片的坐标是没变的,变的是坐标系

- (void)showanimation{
    CGAffineTransform transform = self.dview.transform;
    transform = CGAffineTransformTranslate(transform, 100, 100);
    [UIView animateWithDuration:.5 animations:^{
        [self.dview setTransform:transform];
    }];
}

再加一个按钮,对dview进行变换,发现CTM不会影响变换的结果

我们看到,CTM的作用在context上的,transform是作用在UIView上的,但是CTM只提供了简单的坐标系变换函数,做不到像CGAffineTransform那样的组合和分解.
对此Core Graphics提供了将Transform转换到CTM的函数,这个转换也包含了执行

  • CGAffineTransform CGContextGetCTM(CGContextRef c);
    获取上下文的变换矩阵

  • void CGContextConcatCTM(CGContextRef c, CGAffineTransform transform);
    CGAffineTransform转换CTM,并执行,设置上下文的变换矩阵

//变换坐标系
        CGAffineTransform transform = CGContextGetCTM(context);
        transform = CGAffineTransformRotate(transform, M_PI_4);
        transform = CGAffineTransformTranslate(transform, 200, 0);
        CGContextConcatCTM(context, transform);
//绘制图形
        CGContextDrawImage(context, CGRectMake(0, 0, 80, 80), [UIImage imageNamed:@"img"].CGImage);

箭头就是图片img

5.3D变换

3D变换是在Core Animation里面的,3D变换在齐次坐标概念中,是4x4的矩阵
Z轴是朝向屏幕的里和外,当然,屏幕是二维的,3d的效果通过光栅化变换到二维的屏幕上.
提供了类似2d变换的一套API;
CALayer的3d变换在各种iOS版本都支持,UIKit支持3d变换需要在iOS13以上,后面Core Animation再讲.

    CATransform3D tran3d = CATransform3DRotate(self.dview.transform3D, M_PI, 100, 100, 100);
    [UIView animateWithDuration:.5 animations:^{
        [self.dview setTransform3D:tran3d];
     }];
3D变换

动画

你可能感兴趣的:(Core Graphics 二: CGAffineTransform变换和CTM坐标系)