Quartz 2D drawRect method (iPhone)

2020-02-07 16:14发布

问题:

I've got 4 different iPhone/Cocoa/Core Animation/Objective-C books in front of me, along with numerous sample code from the web. Yet somehow I still feel like I'm missing some fundamental understanding of how drawing works in Quartz 2D.

Is drawRect() meant to simply be a hook in which to execute your drawing code? Or is this method supposed to also redraw regions that are "damaged", and need repainting? Can I just draw my stuff once and then it "sticks", or must I repaint the whole scene at any time via drawRect()? Java's Graphics2D object works this way- you must draw your whole "image" every time paint() is called, so you must be prepared to re-construct it at any time (or cache it).

How would you implement a simple drawing program? Would you have to "remember" each line/point/stroke that the user drew, and replicate that each time drawRect() is called? How about "offscreen" rendering; can you do all your drawing and then call [self setNeedsDisplay] to have your writes flushed to the screen?

Let's say that in response to a user's touch, I want to put an "X" on the screen where he touched up. The X should remain there, and each new touch produces another X. Do I need to remember all these touchup coordinates and then draw them all in drawRect() ?

EDIT:

Unless I've misunderstood, joconor and Hector Ramos's answers below are contradicting each other. And that's a good demonstration of my confusion concerning this subject. :-)

回答1:

Some of the confusion between various Cocoa references comes from the introduction of layer-backed views in Leopard. On the iPhone, all UIViews are layer-backed, where in Leopard views need to manually enable layer-backing.

For a layer-backed view, content is drawn once using whatever you supplied in drawRect(), but then is buffered into the layer. The layer acts like a rectangular texture, so when you move the layer-backed view or cover it, no redraw is needed, the texture is just moved to that location via the GPU. Unless you set the needsDisplayOnBoundsChange property to YES for a layer, changing the size of the layer (or its containing view) will simply scale the contents. This may lead to blurry graphics within your view or layer, so you may want to force a redraw in this case. setNeedsDisplay will trigger a manual redraw of the view's or layer's content, and a subsequent recaching of that content in the layer.

For optimal performance, it's suggested that you avoid having frequent calls to drawRect, because Quartz drawing and recaching in a layer are expensive operations. It's best to try to do animation using separate layers that you can move around or scale.

The Cocoa-based references you've seen that relate to the desktop may assume non-layer-backed views, which do call drawRect: any time the view needs to be updated, whether that's from movement, scaling, or having part of the view obscured. As I said, all UIViews are layer-backed, so this is not the case on the iPhone.

That said, for your drawing application, one way to do it would be to maintain an array of drawn objects and call drawRect: each time the user adds something new, iterating over each of the previously drawn objects in order. I might suggest an alternative approach where you create a new UIView or CALayer for each drawing operation. The contents of that drawing operation (line, arc, X, etc.) would be drawn by the individual view or layer. That way, you won't have to redraw everything on a new touch, and you might be able to do some neat vector-style editing by moving each of the drawn elements around independently of the others. For complex drawings, there might be a bit of a memory tradeoff in this, but I'd bet that it would have much better drawing performance (minimal CPU usage and flickering).



回答2:

drawRect() will draw to the offscreen buffer. You don't need to redraw any time the regions are "damaged" perse, as the iPhone OS takes care of handling the layering of the views. You just write once to the buffer, and let the OS handle the rest. This is not like other programming environments where you need to keep redrawing whenever something passes over your view.



回答3:

Always be prepared to draw the appropriate area of your view when drawRect: is called.

Although the system may buffer your view, that will only avoid drawRect: from being invoked. If for some reason, the system has to invalidate the buffer, your drawRect: method may be invoked again. Also, drawRect: will be invoked for different areas of your view as they become visible as a result of scrolling and other operations that affect the visibility of areas of your view.



回答4:

Is drawRect() meant to simply be a hook in which to execute your drawing code?

It is meant to redraw the region (rect) that is passed to you, using the current graphics context stack. No more.

Or is this method supposed to also redraw regions that are "damaged", and need repainting?

No. If dirty regions do not overlap, you may receive multiple invocations of drawRect: with different rects passed to you. Rects are invalidated using setNeedsDisplayInRect:. If only a portion of your view's surface must be redrawn, then you will be requested to draw that portion when it is time to draw.

Can I just draw my stuff once and then it "sticks", or must I repaint the whole scene at any time via drawRect()?

It does not 'stick'. Rects become invalidated during your app's execution, and you are requested to redraw those rects when the view system needs to update the screen. You repaint only the rect that is requested.

In some cases, a simple implementation may (rather lazily) invalidate the view's entire rect whenever a portion is invalidated. That's typically bad because it typically requires more drawing than is necessary, and is particularly wasteful when views are not opaque.

Java's Graphics2D object works this way- you must draw your whole "image" every time paint() is called, so you must be prepared to re-construct it at any time (or cache it).

Not so with AppKit or UIKit.

How would you implement a simple drawing program? Would you have to "remember" each line/point/stroke that the user drew, and replicate that each time drawRect() is called?

You will have to remember the context (e.g. each line/point/stroke) required to draw your view. You only need to draw the region that is requested. Technically, the graphics system would not complain if you were to draw outside that rect, but that could lead to artifacts.

For complex rendering, it may be easier or more efficient to draw to an external buffer (e.g. bitmap), then to use that prerendered bitmap representation to get things onscreen while in drawrect:. (see also Brad's answer for layers)

How about "offscreen" rendering; can you do all your drawing and then call [self setNeedsDisplay] to have your writes flushed to the screen?

Yes, you can do that. Specifically, you would render to an external buffer (e.g. a bitmap), when you are finished rendering to the bitmap, invalidate the rect you wish to draw, then draw to the screen using the data in the bitmap when drawRect: is called.

Let's say that in response to a user's touch, I want to put an "X" on the screen where he touched up. The X should remain there, and each new touch produces another X. Do I need to remember all these touchup coordinates and then draw them all in drawRect() ?

Well, you have a few options. Your proposal is one (assuming you draw within the rect passed to you). Another would be to create an 'X' view, and simply remember the points needed to reconstruct the view if you need those Xs to persist across launches. In many cases, you can easily divide complex problems into layers (simple 2D game):

  • 1) The background image with the horizon.
  • 2) Some things in the foreground that don't change frequently.
  • 3) The character the player the user uses to navigate through the game.

So, most problems can be easily divided so you don't have to render everything all the time. This reduces complexity and improves performance if done well. If done poorly, it can be several much worse.