iOS 绘画学习(4)


颜色和图案

一个颜色就是一个CGColor(实际上是CGColorRef)。CGColor非常容易使用,而且也可以通过UIColor的 colorWithCGColor: 方法和 CGColor的相关方法来相互转换。

一个图案其实就是一个CGPattern(实际上是CGPatternRef)。你可以创建一个图案并描边或者填充它。这个过程非常复杂,这里我把箭头变为一个红,蓝相接的三角形来说明,为了绘制这个图案,把下面这一行去掉:

CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);

在这一行的位置,写上如下代码:

CGColorSpaceRef sp2 = CGColorSpaceCreatePattern(nil);
CGContextSetFillColorSpace (con, sp2);
CGColorSpaceRelease (sp2);
CGPatternCallbacks callback = {
    0, drawStripes, nil
};
CGAffineTransform tr = CGAffineTransformIdentity;
CGPatternRef patt = CGPatternCreate(nil,  CGRectMake(0,0,4,4), tr, 4, 4, kCGPatternTilingConstantSpacingMinimalDistortion, true, &callback);
CGFloat alph = 1.0;
CGContextSetFillPattern(con, patt, &alph);
CGPatternRelease(patt);

上面的代码有点啰嗦,但几乎可以算是一个完整的样板。为了理解这段代码,我们从后面读起。我们看到 CGContextSetFillPattern,设置一个填充图案,而不是一个填充颜色来填充一个路径(这里就是一个三角箭头)。第三个参数是一个指向CGFloat的指针,所以我们需要在之前设置这个CGFloat。第二个参数是一个CGPatternRef,所以我们需要在之前创建一个CGPatternRef(然后在之后释放它)。

现在让我们来看看这个CGPatternCreate调用。一个图案是绘制在一个三角形的“单元”里;我们必须确定这个单元的大小(第二个参数),还有每个单元原点之间的距离(第四和第五个参数)。这样,这个单元就是4x4的,每个单元在垂直和水平方向上都紧靠一起。我们还需要提供一个转换来应用到单元上(第三个参数)。我们还提供一个平铺规则(第六个参数)。我们必须确定这是一个颜色图案,还是一个模版图案,如果是颜色图案,第七个参数就是true。我们还要提供一个指向回调函数的指针,这个函数就是实际上在单元里绘制图案(第八个参数)。

对于第八个参数,为了让事情现在变得不那么复杂,我们实际上只是提供了一个指向CGPatternCallbacks 结构体的指针,这个结构体包含一个数字0,和两个函数的指针,一个会被调用来在它的单元里绘制图案,另一个会在这个图案释放的时候被调用。这里我们没有指定第二个参数,因为是用来内存管理的,这个简单的例子里不需要用到。

往前一点,我们还需要给一个图案的色彩空间设置上下文的填充色彩空间。如果你忽略了这个,当你调用CGContextSetFillPattern.时会得到一个错误。所以我们创建一个色彩空间,设置它为上下文的填充色彩空间,然后释放它。

但是我们还没有完成,因为我还没有向你展示实际绘制图案单元的方法!

void drawStripes (void *info, CGContextRef con) {
    // assume 4 x 4 cell
    CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);
    CGContextFillRect(con, CGRectMake(0,0,4,4));
    CGContextSetFillColorWithColor(con, [[UIColor blueColor] CGColor]);
    CGContextFillRect(con, CGRectMake(0,0,4,2));
}

如你所见,实际的图案代码非常简单,唯一需要注意的地方就是绘制的大小必须和CGPatternCreate创建的图案时指定的单元大小一样,否则绘制出来的图案不是我们想要的。这里我们的单元大小是4x4,所以我们用红色填充它,然后用蓝色填充下半部。当这些单元在水平和垂直方向上紧挨着平铺后,就是我们看到的图案。

最后,需要注意,我们应该用 CGContextSaveGState 和 CGContextRestoreGState.把我们的代码包起来。

你可能注意到上图中的条纹并没有刚好适应三角形的箭头里,底部的一些条纹像是半个蓝色条纹。这是因为一个图案并不是绘制在你指定填充或者描边的形状里,而是整个图形上下文。我们可以通过CGContextSetPatternPhase 来在绘制之前移动这个图案的位置。

对于一个这么简单的图案,我们也可以很简单地通过UIColor的 colorWithPatternImage:方法来获得一个UIImage:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(4,4), NO, 0);
drawStripes(nil, UIGraphicsGetCurrentContext());
UIImage* stripes = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIColor* stripesPattern = [UIColor colorWithPatternImage:stripes];
[stripesPattern setFill];
UIBezierPath* p = [UIBezierPath bezierPath];
[p moveToPoint:CGPointMake(80,25)];
[p addLineToPoint:CGPointMake(100,0)];
[p addLineToPoint:CGPointMake(120,25)];
[p fill];

图形上下文转换

正如UIView可以有转换,图形上下文也可以有转换。但是,给一个图形上下文提供一个转换并不会影响已经绘制在上面的图像,它仅仅影响之后的绘制。一个图形上下文的转换,称为这个图形上下文的CTM,“current transformation matrix 当前的转换矩阵”。

充分利用图形上下文的CTM,可以减少你的很多计算。你可以调用CGContextConcatCTM 用任意的CGAffineTransform 乘以当前的转换;还有很多方便的函数对当前的转换进行平移,拉伸和旋转。

当你获得一个上下文时,一个基本的转换已经设置好了,使得系统能够把上下文的绘制坐标和屏幕的坐标对应起来。所有的转换都是应用到当前的转换,所有这个基本的转换仍然有效。通过用CGContextSaveGState 和 CGContextRestoreGState.包围起你的代码,你可以在最后恢复原始的基本转换。

例如,我们的向上箭头,左上方的三角形位置是写死{80,0}。这样不好理解,为了能够更好地重用我们的代码,我们可以理解箭头原来的绘制坐标是{0,0},只是在x轴方向上平移了80.那么现在把箭头绘制在{80,0}的位置,可以使用转换:

CGContextTranslateCTM(con, 80, 0);
// now draw the arrow at (0,0)

下面重复绘制不同旋转角度下的箭头。由于这个箭头需要绘制多次,我会把这个箭头包装成一个UIImage,这样可以大大减少重复工作,同时也让绘制更快

- (UIImage*) arrowImage {
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(40,100), NO, 0.0);
    // obtain the current graphics context
    CGContextRef con = UIGraphicsGetCurrentContext();
    // draw the arrow into the image context
    // draw it at (0,0)! adjust all x-values by subtracting 80
    // ... actual code omitted ...
    UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return im;
}

我们生成了这个箭头的图像,把它保存在一个实例变量中,用self.arrow来访问:

- (void)drawRect:(CGRect)rect {
    CGContextRef con = UIGraphicsGetCurrentContext();
    [self.arrow drawAtPoint:CGPointMake(0,0)];
    for (int i=0; i<3; i++) {
        CGContextTranslateCTM(con, 20, 100);
        CGContextRotateCTM(con, 30 * M_PI/180.0);
        CGContextTranslateCTM(con, -20, -100);
        [self.arrow drawAtPoint:CGPointMake(0,0)];
    }
}

转换也可以作为我们之前提到的使用CGContextDrawImage时产生的“反转”问题的一个解决方法。我们反转图形上下文,而不是绘画视图。你可以向下移动上下文的顶部,然后通过拉伸转换,反转y坐标方向:

CGContextTranslateCTM(con, 0, theHeight);
CGContextScaleCTM(con, 1.0, -1.0);

至于你想顶部移动多少(theHeight)取决于你打算怎么绘制图像。

posted @ 2014-07-17 14:34  剑尖  阅读(567)  评论(0编辑  收藏  举报