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中[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轴移动tyCGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformScale(CGAffineTransform t,
CGFloat sx, CGFloat sy)
缩放变换,每个点的x乘上sx,y乘上syCGAffineTransformMakeRotation(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));
}];
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);
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);
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];
}];