Core Graphics 二: CGAffineTransform變換和CTM坐標(biāo)系

Core Graphics 一: CGContext基本繪制

圖形學(xué)變換需要了解矩陣
1.向量和矩陣
2.變換

變換的過(guò)程是圖形的每一個(gè)點(diǎn)都根據(jù)一個(gè)關(guān)系變換成另一個(gè)點(diǎn)
在齊次坐標(biāo)中,點(diǎn)可以被描述成矩陣
變換的過(guò)程就是點(diǎn)的矩陣乘上一個(gè)變換矩陣(變換關(guān)系),最后得到另給一個(gè)點(diǎn)矩陣

CGAffineTransform

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

CGAffineTransform提供了用于創(chuàng)建、連接和應(yīng)用仿射轉(zhuǎn)換的函數(shù).
因?yàn)榈谌锌偸?0,0,1)臣疑,所以CGAffineTransform數(shù)據(jù)結(jié)構(gòu)只包含前兩列的值.
注意文檔中的寫(xiě)法,矩陣寫(xiě)成一排,是從左到右從上到下的,比如一個(gè)3x3的單位矩陣,單位矩陣是對(duì)角線(xiàn)為1,其他全是0的矩陣,在iOS的文檔中寫(xiě)作[1 0 0 0 1 0 0 0 1]
所以3x2的CGAffineTransform寫(xiě)作[a b c d tx ty]

在上面的文章中,點(diǎn)被描述成單列矩陣,而在Quartz中,點(diǎn)描述成單行矩陣因此iOS的變換矩陣和上面文章里的變換矩陣不太一樣.


開(kāi)頭鏈接里的

Quartz里的

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

綜上所述,在Quartz中,變換的過(guò)程就是調(diào)用CGAffineTransform的一系列方法,生成一個(gè)CGAffineTransform結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體就是一個(gè)變換矩陣,然后設(shè)置UIView的UIViewGeometry分類(lèi)里的transform屬性,或者調(diào)用變換函數(shù)CTM,給定上下文和CGAffineTransform,來(lái)執(zhí)行變換

1.CGAffineTransform的函數(shù)

  • CGAffineTransformIdentity
    變換的初始狀態(tài),行向量乘上這個(gè)矩陣什么都不會(huì)發(fā)生,也就是[ 1 0 0 1 0 0]
    CGAffineTransform transform = CGAffineTransformIdentity;
    [UIView animateWithDuration:.5 animations:^{
        [view setTransform:CGAffineTransformScale(transform, .5, .5)];
    }];

CGAffineTransform提供了幾個(gè)生成特定形式變換矩陣的API

  • CGAffineTransform CGAffineTransformMakeTranslation(CGFloat tx,
    CGFloat ty)
    CGAffineTransformTranslate(CGAffineTransform t,
    CGFloat tx, CGFloat ty)
    平移變換,每個(gè)點(diǎn)在x軸移動(dòng)tx,在y軸移動(dòng)ty

  • CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
    CGAffineTransformScale(CGAffineTransform t,
    CGFloat sx, CGFloat sy)
    縮放變換,每個(gè)點(diǎn)的x乘上sx,y乘上sy

  • CGAffineTransformMakeRotation(CGFloat angle)
    CGAffineTransformRotate(CGAffineTransform t,
    CGFloat angle)
    旋轉(zhuǎn)變換,提供一個(gè)角度,旋轉(zhuǎn)變換的矩陣是[cosθ -sinθ sinθ cosθ],不過(guò)這里不需要計(jì)算矩陣,參數(shù)angle是π,
    即M_PI=180°,是順時(shí)針計(jì)算的

  • CGAffineTransformInvert(CGAffineTransform t)
    反向變換,這個(gè)函數(shù)是對(duì)變換矩陣的一種運(yùn)算,
    當(dāng)t是一個(gè)縮放矩陣的時(shí)候,比如放大3倍,調(diào)用這個(gè)函數(shù)之后會(huì)變成放大1/3倍,
    當(dāng)t是一個(gè)平移矩陣時(shí),假設(shè)tx=150,ty=200,調(diào)用函數(shù)之后,tx變成-150,ty變成-200
    當(dāng)t是一個(gè)旋轉(zhuǎn)矩陣是,調(diào)用函數(shù)后,會(huì)變成逆時(shí)針旋轉(zhuǎn)

  • CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2)
    合并兩次變換,也就是將兩個(gè)變換矩陣相乘,注意矩陣乘法不遵循交換率,AxB != BxA,因此變換的順序很重要,具體參考本文開(kāi)頭的鏈接

  • CGAffineTransformEqualToTransform(CGAffineTransform t1, CGAffineTransform t2)
    對(duì)比兩個(gè)變換矩陣是否相同

  • CGPointApplyAffineTransform(CGPoint point, CGAffineTransform t)
    對(duì)CGPoint使用變換

  • CGSizeApplyAffineTransform(CGSize size, CGAffineTransform t)
    對(duì)CGSize使用變換

  • CGRectApplyAffineTransform(CGRect rect, CGAffineTransform t)
    對(duì)CGRect使用變換

  • CGAffineTransformMake(CGFloat a, CGFloat b, CGFloat c, CGFloat d, CGFloat tx, CGFloat ty)
    自定義一個(gè)變換矩陣
    CGAffineTransform只提供了生成平移旋轉(zhuǎn)等比縮放的函數(shù),切變和鏡像等則需要自己寫(xiě)CGAffineTransformMake
    CGAffineTransformMake(-1, 0, 0, 1, 0, 0); //翻轉(zhuǎn)矩陣,在Quartz中是繞豎直對(duì)稱(chēng)軸翻轉(zhuǎn)
    CGAffineTransformMake(1, 0, -.3, 1, 0, 0);//切變矩陣

2.變換的拆分與合并
變換就是矩陣,兩次變換就是兩個(gè)矩陣,那么,兩個(gè)矩陣相乘,就是將兩個(gè)變換合并起來(lái),一次完成,
同理,復(fù)雜的變化也可以拆分
例如,一個(gè)平移矩陣[1 0 0 1 2 2] ,乘上第二個(gè)平移矩陣[1 0 0 1 2 2] 等于[1 0 0 1 4 4].
因此,上面的Translate和Scale和Rotate三個(gè)函數(shù),都提供了兩個(gè)函數(shù),一個(gè)是需要一個(gè)CGAffineTransform,修改并返回,另一個(gè)不需要傳CGAffineTransform,它會(huì)修改CGAffineTransformIdentity并返回,因此一個(gè)是疊加,一個(gè)是從初始狀態(tài)計(jì)算.
當(dāng)分解一個(gè)變換的時(shí)候,就需要操作同一個(gè)CGAffineTransform結(jié)構(gòu)體

    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.變換對(duì)UIKit坐標(biāo)系的影響
拆分變換需要注意坐標(biāo)系的變化,當(dāng)使用旋轉(zhuǎn)變換后,坐標(biāo)系也會(huì)發(fā)生旋轉(zhuǎn).

    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));
    }];

在這個(gè)例子里面,view先平移,再旋轉(zhuǎn),再平移會(huì)來(lái),其中:
1.旋轉(zhuǎn)始終是以view的中心為中心進(jìn)行
2.view的fram在變換前是{{362, 483}, {300, 400}},旋轉(zhuǎn)90度后,frame變成{{312, 533}, {400, 300}},寬高顛倒了.
那么如果旋轉(zhuǎn)45度呢,CGRec在坐標(biāo)系內(nèi)是一個(gè)平行于坐標(biāo)軸的矩形,旋轉(zhuǎn)45度就不平行了,改成M_PI_4打印一下,發(fā)現(xiàn)是{{264.51262658470836, 435.51262658470836}, {494.97474683058323, 494.97474683058323}},
這不太容易看出來(lái),把寬高都改成300,再試一次,從{{362, 483}, {300, 400}}變成了{(lán){299.86796564403573, 470.86796564403573}, {424.26406871192853, 424.26406871192853}},一個(gè)正方形旋轉(zhuǎn)45度之后,對(duì)角線(xiàn)垂直于y軸,300x300+300x300 = 424x424,因此view的fram變成了一個(gè)包裹變換后的區(qū)域的新的矩形.
為了驗(yàn)證這一點(diǎn),可以再創(chuàng)建一個(gè)viewA,始終讓A的fram和View的frame相同,A也添加在VC上,給A一個(gè)透明度方便觀察,然后執(zhí)行動(dòng)畫(huà).

 [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,因?yàn)樽鴺?biāo)系的方向變了,一開(kāi)始左上角是原點(diǎn),朝右是x,朝下是y,旋轉(zhuǎn)后,右上角是原點(diǎn),朝左是y,朝下是x.
transform結(jié)構(gòu)體在調(diào)用CGAffineTransform函數(shù)之后,會(huì)將transform進(jìn)行修改并返回,而CGAffineTransformmake函數(shù)不需要transform參數(shù),也就不會(huì)修改,它只是返回一個(gè)變換矩陣而已,這兩種的結(jié)果是不一樣的,一個(gè)是在變換的基礎(chǔ)上繼續(xù)變換,另一個(gè)是在初始狀態(tài)的基礎(chǔ)上變換.

//        transform = CGAffineTransformTranslate(transform, 200, 0);
        [UIView animateWithDuration:.5 animations:^{
//            [self.dview setTransform:transform];
            [self.dview setTransform:CGAffineTransformMakeTranslation(200, 0)];
        }];
第一次的旋轉(zhuǎn)變換被丟掉了

在第一次變換的基礎(chǔ)上第二次變換

當(dāng)然,還可以合并兩次變換

    transform = CGAffineTransformRotate(transform, M_PI_4);
    transform = CGAffineTransformTranslate(transform, 200, 0);
合并變換矩陣.gif

4.由于view旋轉(zhuǎn)了,因此view在superView中的frame也發(fā)生了變化,origin和size可能都變了,但是view自身的坐標(biāo)系變了,因此view中的subView的frame沒(méi)有變,同樣view的bounds是基于自身坐標(biāo)系的,因此bounds的size不變.

4.CTM坐標(biāo)系
Core Graphics提供了幾個(gè)變換坐標(biāo)系的函數(shù),也就是CTM函數(shù)

  • void CGContextTranslateCTM(CGContextRef cg_nullable c,
    CGFloat tx, CGFloat ty)
    平移坐標(biāo)系

  • void CGContextScaleCTM(CGContextRef c, CGFloat sx, CGFloat sy);
    縮放坐標(biāo)系

  • void CGContextRotateCTM(CGContextRef c, CGFloat angle);
    旋轉(zhuǎn)坐標(biāo)系
    因?yàn)樾D(zhuǎn)的是坐標(biāo)系,所以視覺(jué)效果上是以左上角(0,0)為中心進(jìn)行的,這與UIView不同,angle正是順時(shí)針,負(fù)是逆時(shí)針.

這三個(gè)函數(shù)的功能就是將繪制時(shí)使用的坐標(biāo)系(CTM)進(jìn)行變換,與UIKit的坐標(biāo)系不同,CTM發(fā)生變化,對(duì)UIKit坐標(biāo)沒(méi)有影響.視圖的子視圖沒(méi)有任何變化,frame也不會(huì)變化,但是context繪制的圖形會(huì)變化.由于是坐標(biāo)系的變化,所以圖形的位置和大小都可能會(huì)變

///View內(nèi)
- (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內(nèi)
- (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定義一個(gè)shouldTransform屬性,在VC創(chuàng)建一個(gè)按鈕,按鈕點(diǎn)擊會(huì)設(shè)置shouldTransform為yes,然后重新繪制,并且再打印一次subV的frame
結(jié)果是subV的frame和視覺(jué)都沒(méi)有變換,但是繪制在dview上的圖片偏移了
在調(diào)用CTM函數(shù)前后,繪制圖片的rect都是CGRectMake(rect.size.width/2, rect.size.height/2, 80, 80),因此圖片的坐標(biāo)是沒(méi)變的,變的是坐標(biāo)系

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

再加一個(gè)按鈕,對(duì)dview進(jìn)行變換,發(fā)現(xiàn)CTM不會(huì)影響變換的結(jié)果

我們看到,CTM的作用在context上的,transform是作用在UIView上的,但是CTM只提供了簡(jiǎn)單的坐標(biāo)系變換函數(shù),做不到像CGAffineTransform那樣的組合和分解.
對(duì)此Core Graphics提供了將Transform轉(zhuǎn)換到CTM的函數(shù),這個(gè)轉(zhuǎn)換也包含了執(zhí)行

  • CGAffineTransform CGContextGetCTM(CGContextRef c);
    獲取上下文的變換矩陣

  • void CGContextConcatCTM(CGContextRef c, CGAffineTransform transform);
    CGAffineTransform轉(zhuǎn)換CTM,并執(zhí)行,設(shè)置上下文的變換矩陣

//變換坐標(biāo)系
        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變換在齊次坐標(biāo)概念中,是4x4的矩陣
Z軸是朝向屏幕的里和外,當(dāng)然,屏幕是二維的,3d的效果通過(guò)光柵化變換到二維的屏幕上.
提供了類(lèi)似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變換

動(dòng)畫(huà)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末工禾,一起剝皮案震驚了整個(gè)濱河市词疼,隨后出現(xiàn)的幾起案子驯击,更是在濱河造成了極大的恐慌携兵,老刑警劉巖蕾哟,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異共缕,居然都是意外死亡洗出,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)图谷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)翩活,“玉大人,你說(shuō)我怎么就攤上這事便贵〔ふ颍” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵承璃,是天一觀的道長(zhǎng)利耍。 經(jīng)常有香客問(wèn)我,道長(zhǎng)盔粹,這世上最難降的妖魔是什么隘梨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮玻佩,結(jié)果婚禮上出嘹,老公的妹妹穿的比我還像新娘。我一直安慰自己咬崔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著垮斯,像睡著了一般郎仆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上兜蠕,一...
    開(kāi)封第一講書(shū)人閱讀 51,754評(píng)論 1 307
  • 那天扰肌,我揣著相機(jī)與錄音,去河邊找鬼熊杨。 笑死曙旭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的晶府。 我是一名探鬼主播桂躏,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼川陆!你這毒婦竟也來(lái)了剂习?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤较沪,失蹤者是張志新(化名)和其女友劉穎鳞绕,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體尸曼,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡们何,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了控轿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垂蜗。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖解幽,靈堂內(nèi)的尸體忽然破棺而出贴见,到底是詐尸還是另有隱情,我是刑警寧澤躲株,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布片部,位于F島的核電站,受9級(jí)特大地震影響霜定,放射性物質(zhì)發(fā)生泄漏档悠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一望浩、第九天 我趴在偏房一處隱蔽的房頂上張望辖所。 院中可真熱鬧,春花似錦磨德、人聲如沸缘回。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)酥宴。三九已至啦吧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拙寡,已是汗流浹背授滓。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肆糕,地道東北人般堆。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像诚啃,于是被迫代替她去往敵國(guó)和親淮摔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355