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

问题描述 投票:71回答:7

我有一个类,它是UIView的子类。我可以通过实现drawRect方法或通过实现drawLayer:inContext:CALayer的委托方法)来绘制视图中的内容。

我有两个问题:

  1. 如何决定使用哪种方法?每个都有一个用例吗?
  2. 如果我实现drawLayer:inContext:,它被调用(并且drawRect不是,至少就断点可以告诉),即使我没有通过使用以下方式将我的视图指定为CALayer委托: [[self layer] setDelegate:self]; 如果我的实例未被定义为图层的委托,那么如何调用委托方法?如果调用drawRect,有什么机制阻止drawLayer:inContext:被调用?
iphone ios delegates core-animation
7个回答
72
投票

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

总是使用drawRect:,永远不要使用UIView作为任何CALayer的绘图代表。

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

每个UIView实例都是其支持CALayer的绘图代表。这就是为什么[[self layer] setDelegate:self];似乎什么都不做。这是多余的。 drawRect:方法实际上是视图层的绘图委托方法。在内部,UIView实现了drawLayer:inContext:,在那里它做了一些自己的东西,然后调用drawRect:。您可以在调试器中看到它:

这就是为什么drawRect:在你实施drawLayer:inContext:时从未被调用过的原因。这也是为什么你永远不应该在自定义CALayer子类中实现任何UIView绘图委托方法的原因。您也应该永远不要查看另一个图层的绘图委托。这将导致各种古怪。

如果您正在实施drawLayer:inContext:,因为您需要访问CGContextRef,您可以通过调用drawRect:UIGraphicsGetCurrentContext()内部获取。


44
投票

drawRect只应在绝对需要时实施。 drawRect的默认实现包括许多智能优化,例如智能地缓存视图的渲染。覆盖它会绕过所有这些优化。那很糟。有效地使用图层绘制方法几乎总是优于自定义drawRect。 Apple经常使用UIView作为CALayer的代表 - 事实上,每个UIView is the delegate of it's layer。您可以在几个Apple示例中看到如何在UIView中自定义图层绘图,包括(此时)ZoomingPDFViewer。

尽管drawRect的使用很常见,但至少从2002/2003年IIRC开始,这种做法一直被劝阻。沿着这条道路走的路没有很多好理由。

Advanced Performance Optimization on iPhone OS(幻灯片15)

Core Animation Essentials

Understanding UIKit Rendering

Technical Q&A QA1708: Improving Image Drawing Performance on iOS

View Programming Guide: Optimizing View Drawing


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
{
    ...
}

8
投票

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

今天我在实施my own Label class时遇到了与这两个函数相关的各种渲染问题。在查看文档,做一些反复试验,反编译UIKit并检查Apple的Custom Animatable Properties example后,我对它的工作原理有了很好的认识。

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.layer的视图的layerdrawLayer(_:inContext:)参数并不总是同一层!后者可能是前者的副本,部分动画已经应用于其属性。这样您就可以访问正在进行的动画的正确属性值。

现在绘图按预期工作:

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

但是有两个新问题:setNeedsDisplay()backgroundColoropaque等几个属性不再适用于您的观点。 UIView不再将呼叫和更改转发到自己的层。

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

这些属性可能不再起作用,因为不再调用UIView自己的drawLayer(_: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:的默认实现调用drawRect:,或drawRect:仅在您的子类未实现drawLayer:inContext:时调用。

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

这并不重要。为了遵循惯例,我通常会使用drawRect:并保留使用drawLayer:inContext:,因为我实际上必须绘制不属于视图的自定义子图层。


2
投票

Apple Documentation有这样说:“还有其他方法可以提供视图的内容,例如直接设置底层的内容,但是覆盖drawRect:方法是最常用的技术。”

但它没有涉及任何细节,所以这应该是一个线索:除非你真的想弄脏你的手,否则不要这样做。

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


0
投票

对于具有自定义内容的图层支持视图,您应该继续覆盖视图的方法来进行绘制。图层支持的视图自动使其自身成为其图层的委托,并实现所需的委托方法,您不应更改该配置。相反,您应该实现视图的drawRect:方法来绘制content.Core Animation Programming Guide

© www.soinside.com 2019 - 2024. All rights reserved.