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变换

动画
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335