如何创建一个适用于Retina显示屏的CGBitmapContext,不浪费在普通显示屏上的空间?

15

如果使用UIKit,包括drawRect,它是否会自动处理Retina显示器的高清晰度?这是否意味着在drawRect中,1024 x 768视图的当前图形上下文实际上是一个2048 x 1536像素的位图上下文?

(更新:如果我在drawRect中使用当前上下文创建一个图像并打印其大小:

CGContextRef context = UIGraphicsGetCurrentContext();
CGImageRef image = CGBitmapContextCreateImage(context);
NSLog(@"width of context %i", (int) CGImageGetWidth(image));
NSLog(@"height of context %i", (int) CGImageGetHeight(image));

在新版iPad上,如果状态栏被禁用,将会显示2048和1536,而iPad 2将显示1024和768。

我们其实享受到了1点=4像素的奢侈,这一切都被自动处理。

然而,如果我们使用,那么它们将真正成为像素而不是点?(至少如果我们为该位图提供数据缓冲区,则缓冲区的大小(字节数)显然不是针对更高分辨率的,而是普通分辨率,即使我们将null作为缓冲区传递给以便由它来处理缓冲区,大小可能与我们传递数据缓冲区时相同,因为它只是标准分辨率,而不是Retina的分辨率)。

我们总是可以为iPad 1和iPad 2以及新版iPad创建2048 x 1536,但这将会浪费内存、处理器和GPU的功率,因为它只有在新版iPad上才需要。

那么我们必须使用if () { } else { }来创建这样的位图上下文,我们如何实际做到这一点呢?而所有的代码CGContextMoveToPoint都必须针对Retina显示做出调整,使用x * 2y * 2而不是非Retina显示的只使用x, y。这可能会让代码变得很混乱。(或者我们可以定义一个本地变量scaleFactor并将其设置为[[UIScreen mainScreen] scale],因此它在标准分辨率下为1,在Retina下为2,因此我们的xy将始终为x * scaleFactory * scaleFactor,而不仅仅是在使用CGContextMoveToPoint等进行绘制时使用xy)。

似乎UIGraphicsBeginImageContextWithOptions如果传入0.0的比例尺,则可以自动创建Retina版本,但我认为如果需要创建上下文并保留它(并使用UIViewController的ivar或property来保存它),则无法使用它。如果我不使用UIGraphicsEndImageContext释放它,则它将保留在图形上下文堆栈中,因此似乎我必须使用代替。(或者我们只是让它保持在堆栈底部,不用担心它吗?)

4个回答

41

做了更多研究后,我找到了以下解决方案:

如果必须使用CGBitmapContextCreate,则有两个步骤可以使上下文大小和坐标系统适合标准显示屏或Retina显示屏:

float scaleFactor = [[UIScreen mainScreen] scale];

CGSize size = CGSizeMake(768, 768);

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

CGContextRef context = CGBitmapContextCreate(NULL, 
                           size.width * scaleFactor, size.height * scaleFactor, 
                           8, size.width * scaleFactor * 4, colorSpace, 
                           kCGImageAlphaPremultipliedFirst);

CGContextScaleCTM(context, scaleFactor, scaleFactor);

示例是创建一个768 x 768的区域,在新iPad上它将是1536 x 1536的像素。在iPad 2上,它是768 x 768的像素

一个关键因素是使用CGContextScaleCTM(context, scaleFactor, scaleFactor);来调整坐标系统,这样任何由Core Graphics绘制的内容,例如CGContextMoveToPoint等,将自动适应标准分辨率或Retina分辨率。


还要注意的一点是,UIGraphicsBeginImageContext(CGSizeMake(300, 300));将在Retina显示器上创建一个300 x 300的像素,而UIGraphicsBeginImageContextWithOptions(CGSizeMake(300, 300), NO, 0.0);将在Retina显示器上创建一个600 x 600的像素0.0是用来让方法调用自动给出标准显示器或Retina显示器的正确尺寸。


4
也可以尝试这个:
- (UIImage *)maskImageWithColor:(UIColor *)color
{
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    UIGraphicsBeginImageContextWithOptions(rect.size, NO, self.scale);
    CGContextRef c = UIGraphicsGetCurrentContext();
    [self drawInRect:rect];
    CGContextSetFillColorWithColor(c, [color CGColor]);
    CGContextSetBlendMode(c, kCGBlendModeSourceAtop);
    CGContextFillRect(c, rect);
    UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return result;
}

1

开始新的图像上下文后,您可以使用UIGraphicsGetCurrentContext检索它。然后,如果您想保留它并在此后重复使用它,只需像处理任何CF对象一样对其进行保留(并记得根据规则释放它)。您仍然需要调用UIGraphicsEndImageContext将其从UIKit的上下文堆栈中弹出,但是如果您已经保留了上下文,则上下文将继续存在,并且您应该能够继续使用它直到释放它。

稍后,如果您想再次使用上下文(并且尚未释放它),一种方法是调用UIGraphicsPushContext,它将上下文推回上下文堆栈。

使用上下文的另一种方法是假定它是CGBitmapContext(UIKit文档称其为“基于位图的上下文”,但没有具体指明是CGBitmapContext),并使用CGBitmapContextCreateImage从绘制后的上下文中捕获新图像。

主要区别在于,如果您使用UIGraphicsCreateImageContextWithOptions创建了上下文,则UIGraphicsGetImageFromCurrentImageContext返回的UIImage的scale应与您创建上下文时使用的值相匹配。(我不知道如果您弹出上下文然后稍后将其推回是否会保留该比例值。)CGBitmapContextCreateImage返回一个CGImage,而CGImage只知道像素。
另一个区别是UIKit绘图API(例如UIBezierPath)在UIKit上下文堆栈中的当前上下文中工作。因此,如果您不推送上下文,则只能使用Quartz API来绘制到上下文中。
我没有测试过上述任何内容,因此在编写将交付给用户的代码之前,请自行彻底测试它。

如果使用 UIGraphicsBeginImageContextWithOptions 并且没有立即释放它,那么如果其他上下文被推入堆栈,那么当该上下文不在堆栈顶部时,我们可以使用 UIGraphicsEndImageContext 吗? - nonopolarity
我认为你的意思是,如果我们使用CGBitmapContextCreate,那么是的,我们必须手动处理Retina分辨率,为我们的x和y使用一个scaleFactor? - nonopolarity
但似乎即使我们使用UIGraphicsCreateImageContextWithOptions,如果我们使用CGContextMoveToPoint等进行绘制,仍然需要手动处理x和y,通过x * scaleFactory * scaleFactor - nonopolarity
@動靜能量:使用UIKit API(或要求當前上下文並使用它)的繪圖將進入堆棧中的最頂層上下文。它是一個堆棧的事實告訴您如果推送另一個上下文會發生什麼。至於必須自己應用比例因子,我不認為那是必要的 - 比例因子的整個目的是將輸入中的點坐標轉換為輸出中的像素。 - Peter Hosey
你是否愿意查看我的答案,确认其在技术上是否正确? - nonopolarity

-1
只需使用缩放比例0.0创建上下文即可获得主屏幕的宽度:
UIGraphicsBeginImageContextWithOptions(size,NO,0.0);
CGContextRef context = UIGraphicsGetCurrentContext();

没有第三步。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接