iOS的绘图之drawRect和drawLayer:inContext

作者:Love@YR
链接:http://blog.csdn.net/jingqiu880905/article/details/51851534
请尊重原创,谢谢!

demo下载
一个测试手动调用drawRect方法的demo

这篇文章讲了用Core Graphics来绘制各种图形,可以参考:http://blog.csdn.net/rhljiayou/article/details/9919713

简单的绘图可以直接使用CocoaTouch 层提供的UIKit Framework里的方法来完成。
但UI比较复杂、比较个性化,普通的UI控件无法实现的可以利用Core Graphics Framework来绘制。
Core Graphics是一套基于C的API框架,使用了Quartz2D作为绘图引擎。在Media Layer层。

如果对层的概念比较模糊,建议看下这篇文章的图,很直观。

网上说Quartz2D提供了以下几种类型的Graphics Context:
Bitmap Graphics Context
PDF Graphics Context
Window Graphics Context
Layer Graphics Context
Printer Graphics Context
不知道哪里得来的资料,反正我是没找到那么多。只看到了CGLayerRef(CGLayerCreateWithContext),CGPDFContextCreate,CGBitmapContextCreate即Layer,PDF,Bitmap这3种context

根据这篇文章
的E第4条,drawRect:方法中UIGraphicsGetCurrentContext()取得的是一个Layer Graphics Context。(这样就能理解为啥要画image的时候必须先创建一个image的context)

看下此方法的描述:
You can get a reference to the graphics context using the UIGraphicsGetCurrentContext function, but do not establish a strong reference to the graphics context because it can change between calls to the drawRect: method.
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.

主要就是说你不能手动调drawRect:方法,而是调用setNeedsDisplay去让系统自动调用。

经过上述demo的实践我们看出:
当一个view被addsubview到其他view上时
1.先隐式地把此view的layer的CALayerDelegate设置成此view
2.调用此view的self.layer的drawInContext方法
3.由于drawLayer方法的注释 If defined, called by the default implementation of -drawInContext:
说明了drawInContext里if([self.delegate responseToSelector:@selector(drawLayer:inContext:)])时就执行drawLayer:inContext:方法,这里我们因为实现了drawLayer:inContext:所以会执行
4.[super drawLayer:layer inContext:ctx]会让系统自动调用此view的drawRect:方法
至此self.layer画出来了
5.在self.layer上再加一个子layer来绘图,当调用[layer setNeedsDisplay];时会自动调用此layer的drawInContext方法。drawInContext方法不能手动调用,只能通过这个方法让系统自动调用
6.如果drawRect不重写,就不会调用其layer的drawInContext方法,也就不会调用drawLayer:inContext方法

在UIView的drawRect:rect方法中:
1.直接用UIKit提供的绘图如UIBezierPath去stroke描边,fill填充。
2.也可以用CoreGraphics在ctx上绘图
需要先UIGraphicsGetCurrentContext()得到上下文CGContextDrawPath(ctx,kCGPathFillStroke);//既有填充又有边框

在CALayer的delegate方法drawLayer: inContext:中。
1.直接用UIKit提供的绘图
需要UIGraphicsPushContext(ctx);把当ctx转为当前上下文,可用UIBezierPath的stroke,fill去画,画完图再UIGraphicsPopContext();
2.也可以用CoreGraphics在ctx上绘图
直接用传过来的ctx即可。如:CGContextFillPath(ctx);

在drawLayer: inContext:方法中想要绘制bitmap
需要自己创建bitmap上下文:UIGraphicsBeginImageContextWithOptions
可以用UIImage* img = UIGraphicsGetImageFromCurrentImageContext();来得到image之后再UIGraphicsEndImageContext();

为什么有的时候要UIGraphicsGetCurrentContext,有的时候要push,有的时候直接用呢?我的理解是UIKit和CoreGraphics的上下文不同,座标系也相反,前者y轴向下增大,后者y轴向上增大,所以如果你用UIKit提供的方式绘图和用CoreGraphics提供的方式绘图都需要把传来的ctx转成即将绘图的ctx。
UIGraphicsPushContext 是在UIKit里的方法,无返回,是将传来的上下文CGContextRef转成当前上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();得到的ctx是CoreGraphics里的类型,
drawLayer:inContext里传来的也是CGContextRef类型。

知道了用CGContextAddEllipseInRect来画椭圆之前一定要先UIGraphicsGetCurrentContext或者直接传过来ctx,后来又发现一种画椭圆的方法:CGPathAddEllipseInRect
如下示例方法:

   CGMutablePathRef path=CGPathCreateMutable();
     //2.2把绘图信息添加到路径里
     CGPathMoveToPoint(path, NULL, 20, 20);
     CGPathAddLineToPoint(path, NULL, 200, 300);
    //2.3把路径添加到上下文中
    //把绘制直线的绘图信息保存到图形上下文中
     CGContextAddPath(ctx, path); //仍需把path加入ctx里
     //3.渲染
     CGContextStrokePath(ctx);

参考此篇文章:https://www.mgenware.com/blog/?p=493
http://www.cnblogs.com/wendingding/p/3782679.html

看到UIGraphicsPushContext(ctx)和CGContextSaveGState(ctx)可能会有些疑惑,下面的描述摘抄自以下文章:
http://www.cocoachina.com/industry/20140115/7703.html

使用UiKit,你只能在当前上下文中绘图,所以如果你当前处于UIGraphicsBeginImageContextWithOptions函数或drawRect:方法中,你就可以直接使用UIKit提供的方法进行绘图。如果你持有一个context:参数,那么使用UIKit提供的方法之前,必须将该上下文参数转化为当前上下文。幸运的是,调用UIGraphicsPushContext 函数可以方便的将context:参数转化为当前上下文,记住最后别忘了调用UIGraphicsPopContext函数恢复上下文环境。

因为图形上下文在每一时刻都有一个确定的状态,该状态概括了图形上下文所有属性的设置。为了便于操作这些状态,图形上下文提供了一个用来持有状态的栈。调用CGContextSaveGState函数,上下文会将完整的当前状态压入栈顶;调用CGContextRestoreGState函数,上下文查找处在栈顶的状态,并设置当前上下文状态为栈顶状态。

因此一般绘图模式是:在绘图之前调用CGContextSaveGState函数保存当前状态,接着根据需要设置某些上下文状态,然后绘图,最后调用CGContextRestoreGState函数将当前状态恢复到绘图之前的状态。要注意的是,CGContextSaveGState函数和CGContextRestoreGState函数必须成对出现,否则绘图很可能出现意想不到的错误,这里有一个简单的做法避免这种情况。

但你不需要在每次修改上下文状态之前都这样做,因为你对某一上下文属性的设置并不一定会和之前的属性设置或其他的属性设置产生冲突。你完全可以在不调用保存和恢复函数的情况下先设置线条颜色为红色,然后再设置为蓝色。但在一定情况下,你希望你对状态的设置是可撤销的

举个例子:

-(void)drawRect:(CGRect)rect{ 
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    [[UIColor greenColor] set];
    CGContextSaveGState(ctx); 
    [[UIColor brownColor] set];
    CGContextFillRect(ctx, CGRectMake(0, 0, self.frame.size.width, self.frame.size.height));
    CGContextRestoreGState(ctx);
    CGContextFillRect(ctx, CGRectMake(self.frame.size.width / 2 - 25, self.frame.size.height / 2 - 25, 50, 50));
}

结果是有个绿色的小方框和棕色的整个view,说明restore会用了save之前的颜色。一般用于撤销。
还有一篇文章也可以看下:
http://blog.csdn.net/lihangqw/article/details/9969961

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章