我正在尝试使用Core Graphics构建橡皮擦工具,但我发现制作高性能橡皮擦非常困难 - 这一切都归结为:
CGContextSetBlendMode(context, kCGBlendModeClear)
如果你用谷歌搜索如何使用Core Graphics"擦除",几乎每个答案都会返回该片段。问题是它(显然)仅适用于位图上下文。如果您正在尝试实现交互式擦除,我看不出kCGBlendModeClear
如何帮助您 - 据我所知,您或多或少地被锁定在屏幕内外的擦除UIImage
/CGImage
并在著名的非性能[UIView drawRect]
中绘制该图像。
这是我能做的最好的事情:
-(void)drawRect:(CGRect)rect
{
if (drawingStroke) {
if (eraseModeOn) {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
[eraseImage drawAtPoint:CGPointZero];
CGContextAddPath(context, currentPath);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, lineWidth);
CGContextSetBlendMode(context, kCGBlendModeClear);
CGContextSetLineWidth(context, ERASE_WIDTH);
CGContextStrokePath(context);
curImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[curImage drawAtPoint:CGPointZero];
} else {
[curImage drawAtPoint:CGPointZero];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, currentPath);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, lineWidth);
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGContextSetStrokeColorWithColor(context, lineColor.CGColor);
CGContextStrokePath(context);
}
} else {
[curImage drawAtPoint:CGPointZero];
}
}
绘制一条普通线(!eraseModeOn
)是可以接受的;我正在将屏幕外绘图缓冲区(curImage
,其中包含所有以前绘制的笔触)到当前CGContext
,并且我正在渲染当前正在绘制的线条(路径)。它并不完美,但嘿,它有效,而且性能合理。
但是,由于kCGBlendModeNormal
显然无法在位图上下文之外工作,因此我被迫:
- 创建位图上下文 (
UIGraphicsBeginImageContextWithOptions
)。 - 绘制我的屏幕外缓冲区(
eraseImage
,它实际上是从打开橡皮擦工具时的curImage
派生的 - 所以为了参数起见,它真的与curImage
几乎相同)。 - 渲染当前正在绘制到位图上下文的"擦除线"(路径)(使用
kCGBlendModeClear
清除像素)。 - 将整个图像提取到屏幕外缓冲区 (
curImage = UIGraphicsGetImageFromCurrentImageContext();
) - 然后最后将屏幕外缓冲区传送到视图的
CGContext
这太可怕了,性能方面。使用仪器的时间工具,这种方法的问题在哪里非常明显:
-
UIGraphicsBeginImageContextWithOptions
很贵 - 绘制两次屏幕外缓冲区的成本很高
- 将整个图像提取到屏幕外缓冲区中是昂贵的
因此,自然而然地,代码在真正的iPad上表现得很糟糕。
我真的不确定在这里做什么。我一直在试图弄清楚如何在非位图上下文中清除像素,但据我所知,依赖kCGBlendModeClear
是一个死胡同。
有什么想法或建议吗?其他 iOS 绘图应用程序如何处理擦除?
附加信息
我一直在尝试一种CGLayer
的方法,因为根据我所做的一些谷歌搜索,CGContextSetBlendMode(context, kCGBlendModeClear)
似乎确实可以在CGLayer
中工作。
但是,我并不十分希望这种方法会成功。在drawRect
中绘制图层(即使使用 setNeedsDisplayInRect
)是非常不高性能的;Core Graphics 正在阻塞,将以 CGContextDrawLayerAtPoint
为单位渲染图层中的每个路径(根据 Instruments)。据我所知,就性能而言,使用位图上下文在这里绝对更可取 - 当然,唯一的问题是上述问题(kCGBlendModeClear
将位图上下文传送到drawRect
中的主CGContext
后不起作用)。
我使用以下代码设法获得了良好的结果:
- (void)drawRect:(CGRect)rect
{
if (drawingStroke) {
if (eraseModeOn) {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextBeginTransparencyLayer(context, NULL);
[eraseImage drawAtPoint:CGPointZero];
CGContextAddPath(context, currentPath);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, ERASE_WIDTH);
CGContextSetBlendMode(context, kCGBlendModeClear);
CGContextSetStrokeColorWithColor(context, [[UIColor clearColor] CGColor]);
CGContextStrokePath(context);
CGContextEndTransparencyLayer(context);
} else {
[curImage drawAtPoint:CGPointZero];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, currentPath);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextStrokePath(context);
}
} else {
[curImage drawAtPoint:CGPointZero];
}
self.empty = NO;
}
诀窍是将以下内容包装成CGContextBeginTransparencyLayer
/CGContextEndTransparencyLayer
调用:
- 将擦除背景图像切换到上下文
- 使用
kCGBlendModeClear
在擦除背景图像的顶部绘制"擦除"路径
由于擦除背景图像的像素数据和擦除路径都在同一层中,因此具有清除像素的效果。
遵循绘画范式的2D图形。当你在绘画时,很难去除你已经放在画布上的油漆,但在上面添加更多的油漆非常容易。具有位图上下文的混合模式为您提供了一种使用几行代码即可执行困难操作(从画布上刮除油漆)的方法。几行代码并不能使它成为一个简单的计算操作(这就是为什么它执行缓慢的原因)。
无需进行屏幕外位图缓冲即可假清除像素的最简单方法是在图像上绘制视图的背景。
-(void)drawRect:(CGRect)rect
{
if (drawingStroke) {
CGColor lineCgColor = lineColor.CGColor;
if (eraseModeOn) {
//Use concrete background color to display erasing. You could use the backgroundColor property of the view, or define a color here
lineCgColor = [[self backgroundColor] CGColor];
}
[curImage drawAtPoint:CGPointZero];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, currentPath);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, lineWidth);
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGContextSetStrokeColorWithColor(context, lineCgColor);
CGContextStrokePath(context);
} else {
[curImage drawAtPoint:CGPointZero];
}
}
更困难(但更正确)的方法是在后台串行队列上进行图像编辑以响应编辑事件。获取新操作时,在后台将位图呈现到图像缓冲区。缓冲图像准备就绪后,调用setNeedsDisplay
以允许在下一个更新周期中重绘视图。这更正确,因为drawRect:
应该尽快显示视图的内容,而不是处理编辑操作。
@interface ImageEditor : UIView
@property (nonatomic, strong) UIImage * imageBuffer;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
@end
@implementation ImageEditor
- (dispatch_queue_t) serialQueue
{
if (_serialQueue == nil)
{
_serialQueue = dispatch_queue_create("com.example.com.imagebuffer", DISPATCH_QUEUE_SERIAL);
}
return _serialQueue;
}
- (void)editingAction
{
dispatch_async(self.serialQueue, ^{
CGSize bufferSize = [self.imageBuffer size];
UIGraphicsBeginImageContext(bufferSize);
CGContext context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, CGRectMake(0, 0, bufferSize.width, bufferSize.height), [self.imageBuffer CGImage]);
//Do editing action, draw a clear line, solid line, etc
self.imageBuffer = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
[self setNeedsDisplay];
});
});
}
-(void)drawRect:(CGRect)rect
{
[self.imageBuffer drawAtPoint:CGPointZero];
}
@end
键是CGContextBeginTransparencyLayer并使用clearColor并设置CGContextSetBlendMode(context, kCGBlendModeClear);