iOS:使用UIView的'drawRect:'和其图层的委托'drawLayer:inContext:'


73

我有一类是的子类UIView。我可以通过实现drawRect方法或通过实现drawLayer:inContext:的委托方法在视图内部绘制东西CALayer

我有两个问题:

  1. 如何决定使用哪种方法?每个人都有用例吗?
  2. 如果我实现drawLayer:inContext:drawRect即使我没有使用以下方法将其视图分配为CALayer委托,它也会被调用(至少在设置断点时不会这样):

    [[self layer] setDelegate:self];

    如果未将我的实例定义为图层的委托,则如何调用委托方法?和什么机制阻止drawRectdrawLayer:inContext:调用?

Answers:


71

如何决定使用哪种方法?每个人都有用例吗?

始终使用drawRect:,永远不要将aUIView用作任何图形的委托CALayer

如果未将我的实例定义为图层的委托,则如何调用委托方法?什么机制阻止drawRect被drawLayer:inContext:调用?

每个UIView实例都是其支持的图形委托CALayer。这就是为什么[[self layer] setDelegate:self];似乎什么都不做的原因。这是多余的。该drawRect:方法实际上是视图层的图形委托方法。在内部,UIView实现drawLayer:inContext:在其中执行自己的某些工作然后调用的地方drawRect:。您可以在调试器中看到它:

drawRect:stacktrace

这就是为什么drawRect:在实施时从未调用过的原因drawLayer:inContext:。这也是为什么您永远不要CALayer在自定义中实现任何图形委托方法的原因UIView子类中。您也永远都不应使任何视图代表另一层。那会导致各种各样的古怪。

如果drawLayer:inContext:由于需要访问而正在实施CGContextRef,则可以drawRect:通过调用从内部获取该信息UIGraphicsGetCurrentContext()


我在遵循Apple的代码示例的过程中实现了它AccelerometerGraph。我猜想是在该代码示例中实现的,drawLayer:inContext:因为除了视图的根层之外,还向该层层次结构添加了一些层,而实际上委托不是UIView实例。
Itamar Katz

3
developer.apple.com/library/ios/#DOCUMENTATION/WindowsViews/… “还有其他方法可以提供视图的内容,例如直接设置基础层的内容,但是覆盖drawRect:方法是最常见的技术。 。”
杰米

嗨,Nathan,我对iOS中的自定义绘图非常陌生。我有一个子类化的UIView类,在其中我需要在3个不同的图层上进行绘制。我要制作第一层,在其上绘制一些东西,然后放置第二层,并在其上绘制,然后是第三层。如何从已有的UIView子类中完成此操作?
NSF 2012年

18
这是不正确的答案。只要有可能,我们应该使用CAlayer而不是重写drawRect以避免潜在的性能问题。请参阅Apple的WWDC 2012视频-iOS应用性能:图形和动画(链接到站点developer.apple.com/videos/wwdc/2012)。从11.40处查看,Apple工程师专门解决了这种情况。
user2734323

3
内森,我评论了您的陈述“始终使用drawRect:”。这是不正确的。Apple建议使用CALayer或drawLayer:inContext,除非绝对需要使用drawRect覆盖。有关详细信息,请参见WWDC 2012视频。您是正确的,通过覆盖drawRect(但不是唯一的解决方案)可以轻松实现某些情况,例如矢量绘图。但是,在大多数情况下,CALayer提供了优于drawRect的性能优势。因此,答案应该是“逐案”。
user2734323 2013年

45

drawRect仅应在绝对需要时实施。的默认实现drawRect包括许多智能优化,例如智能缓存视图的呈现。覆盖它可以规避所有这些优化。那很糟。有效地使用图层绘制方法几乎总是会胜过自定义drawRect。Apple通常使用aUIView作为委托CALayer-实际上,每个UIView都是其层的委托。您可以在几个Apple示例(包括ZoomingPDFViewer)中看到如何在UIView中自定义图层绘图。

虽然使用是drawRect很普遍的,但是至少从2002/2003年IIRC开始就不鼓励这样做了。沿着这条路走,没有太多好的理由。

iPhone OS上的高级性能优化(幻灯片15)

核心动画基础

了解UIKit渲染

技术问答QA1708:提高iOS上的图像绘制性能

《 View编程指南:优化View绘图》


此答案与@NathanEror接受的答案相矛盾。在我看来,drawRect应避免的原因是,即使实际上没有任何更改,每次刷新视图时都会调用它。如果没有,drawRect则Core Animation将使用该图层的现有后备存储,该存储可能是先前绘制的。但是我非常困惑,需要一个正式的Apple序列图...有任何指示吗?
AlexChaffee

3
请参阅WWDC 2012会议“ iOS应用程序性能:图形和动画”。这可能与您要寻找的内容非常接近。developer.apple.com/videos/wwdc/2012
quellish

6
OMG是一个很棒的演示!我刚刚升级……我可以感觉到我的生命值和法力值在增加!
AlexChaffee 2013年

12

这是Apple的Sample ZoomingPDFViewer的代码:

-(void)drawRect:(CGRect)r
{

    // UIView uses the existence of -drawRect: to determine if it should allow its CALayer
    // to be invalidated, which would then lead to the layer creating a backing store and
    // -drawLayer:inContext: being called.
    // By implementing an empty -drawRect: method, we allow UIKit to continue to implement
    // this logic, while doing our real drawing work inside of -drawLayer:inContext:

}

-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
    ...
}

如果没有人真正弄清楚,该注释的意思是,它可能会编译出整个drawRect方法(如果已定义),但其中没有任何内容,因此与根本不实现drawRect相同,他们只是插入了它,因此可以解释为什么不使用它。我有些困惑,但我注释掉了整个空的drawRect方法,该程序仍然按原样工作(ZoomingPDFViewer)
mgrandi 2012年

7
否...该注释表示IF -drawRect:存在,UIKit的行为有所不同(即UIKit会查看视图的responsesToSelector drawRect :,并基于此,其CALayer可能无效,也可能不会无效)。
Kalle 2013年

9

是否使用自定义绘图代码drawLayer(_:inContext:)drawRect(_:)(或同时使用)自定义绘图代码取决于是否需要在动画化图层属性时访问其当前值。

今天,当实现自己的Label类时,在与这两个函数相关的各种渲染问题上苦苦挣扎。在检查了文档之后,进行了反复试验,反编译了UIKit并检查了Apple的Custom Animatable Properties示例,我对它的工作方式有了很好的了解。

drawRect(_:)

如果不需要在动画期间访问图层/视图属性的当前值,则可以简单地使用drawRect(_:)它执行自定义绘图。一切都会正常。

override func drawRect(rect: CGRect) {
    // your custom drawing code
}

drawLayer(_:inContext:)

例如,假设您要backgroundColor在自定义绘图代码中使用:

override func drawRect(rect: CGRect) {
    let colorForCustomDrawing = self.layer.backgroundColor
    // your custom drawing code
}

在测试代​​码时,您会注意到 backgroundColor在动画播放过程中该未返回正确的(即当前)值。而是返回最终值(即动画完成时的值)。

为了在动画过程中获得当前值,您必须访问传递给backgroundColorlayer 参数drawLayer(_:inContext:)。而且您还必须吸引context 参数

知道视图self.layerlayer传递给它的参数drawLayer(_:inContext:)并不总是同一层非常重要!后者可能是前者的副本,并且已经对其属性应用了部分动画。这样,您可以访问飞行中动画的正确属性值。

现在,工程图可以按预期工作:

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}

但是有两个新问题:setNeedsDisplay()和一些属性,例如backgroundColoropaque不再对您的视图起作用。UIView不再转发呼叫并更改到其自己的层。

setNeedsDisplay() 仅在您的视图实现时才做某事 drawRect(_:)。函数是否实际执行操作并不重要,但UIKit使用它来确定是否执行自定义绘图。

这些属性可能不再起作用,因为 UIViewdrawLayer(_:inContext:)不再调用自己的实现。

因此,解决方案非常简单。只需调用超类的实现drawLayer(_:inContext:)并实现一个空drawRect(_:)

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}


override func drawRect(rect: CGRect) {
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.
}

概要

使用drawRect(_:)只要你没有这样的属性在动画过程中返回错误值的问题:

override func drawRect(rect: CGRect) {
    // your custom drawing code
}

如果需要动画化视图/图层属性的当前值,请使用drawLayer(_:inContext:) drawRect(_:)

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}


override func drawRect(rect: CGRect) {
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.
}

7

在iOS上,视图与其图层之间的重叠非常大。默认情况下,视图是其图层的委托,并实现该图层的drawLayer:inContext:方法。根据我的理解,drawRect:drawLayer:inContext:在这种情况下或多或少等同。可能是drawLayer:inContext:call的默认实现drawRect:,或者drawRect:drawLayer:inContext:在子类未实现时才调用。

如何决定使用哪种方法?每个人都有用例吗?

没关系。为了遵守约定,当我实际上必须绘制不属于视图的自定义子层时,我通常会使用drawRect:并保留使用drawLayer:inContext:


你打了我几秒钟。:-P
Nathan Eror 2011年

1
@内森:但是你的答案比我的要好。
奥莱·贝格曼

嗨,Ole,我对iOS中的自定义绘图非常陌生。我有一个子类化的UIView类,在其中我需要在3个不同的图层上进行绘制。我要制作第一层,在其上绘制一些东西,然后放置第二层,并在其上绘制,然后是第三层。如何从已有的UIView子类中完成此操作?
NSF 2012年

两种功能有不同的用途。在下面检查我的答案: stackoverflow.com/a/36050120/1183577
fluidsonic

2

苹果文档有这样一段话:“还有其他的方式来提供视图的内容,如直接设置在底层的内容,但重写的drawRect:方法是最常用的技术”

但这并没有涉及任何细节,因此应该提供一个线索:除非您确实想弄脏双手,否则请勿这样做。

UIView的图层的委托指向UIView。但是,根据是否实现了drawRect:,UIView的行为会有所不同。例如,如果您直接在图层上设置属性(例如其背景颜色或其拐角半径),则如果您具有drawRect:方法,则这些值将被覆盖-即使其完全为空(即,甚至不调用super)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.