Core Graphics 一: CGContext基本繪制
變換的過(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的變換矩陣和上面文章里的變換矩陣不太一樣.
因此在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)tyCGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformScale(CGAffineTransform t,
CGFloat sx, CGFloat sy)
縮放變換,每個(gè)點(diǎn)的x乘上sx,y乘上syCGAffineTransformMakeRotation(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));
}];
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)];
}];
當(dāng)然,還可以合并兩次變換
transform = CGAffineTransformRotate(transform, M_PI_4);
transform = CGAffineTransformTranslate(transform, 200, 0);
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);
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];
}];