Drawing incrementally in a UIView (iPhone)
Asked Answered
P

5

11

As far as I have understood so far, every time I draw something in the drawRect: of a UIView, the whole context is erased and then redrawn.

So I have to do something like this to draw a series of dots:

Method A: drawing everything on every call

- (void)drawRect:(CGRect)rect { 

    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextDrawImage(context, self.bounds, maskRef);      //draw the mask
    CGContextClipToMask(context, self.bounds, maskRef);     //respect alpha mask
    CGContextSetBlendMode(context, kCGBlendModeColorBurn);  //set blending mode

    for (Drop *drop in myPoints) {
        CGContextAddEllipseInRect(context, CGRectMake(drop.point.x - drop.size/2, drop.point.y - drop.size/2, drop.size, drop.size));
    }

    CGContextSetRGBFillColor(context, 0.5, 0.0, 0.0, 0.8);
    CGContextFillPath(context);
}

Which means, I have to store all my Dots (that's fine) and re-draw them all, one by one, every time I want to add a new one. Unfortunately this gives my terrible performance and I am sure there is some other way of doing this, more efficiently.

EDIT: Using MrMage's code I did the following, which unfortunately is just as slow and the color blending doesn't work. Any other method I could try?

Method B: saving the previous draws in a UIImage and only drawing the new stuff and this image

- (void)drawRect:(CGRect)rect
{
    //draw on top of the previous stuff
    UIGraphicsBeginImageContext(self.frame.size);
    CGContextRef ctx = UIGraphicsGetCurrentContext(); // ctx is now the image's context
    [cachedImage drawAtPoint:CGPointZero];
    if ([myPoints count] > 0)
    {
        Drop *drop = [myPoints objectAtIndex:[myPoints count]-1];
        CGContextClipToMask(ctx, self.bounds, maskRef);         //respect alpha mask
        CGContextAddEllipseInRect(ctx, CGRectMake(drop.point.x - drop.dropSize/2, drop.point.y - drop.dropSize/2, drop.dropSize, drop.dropSize));
        CGContextSetRGBFillColor(ctx, 0.5, 0.0, 0.0, 1.0);
        CGContextFillPath(ctx);
    }
    [cachedImage release];
    cachedImage = [UIGraphicsGetImageFromCurrentImageContext() retain];
    UIGraphicsEndImageContext();

    //draw on the current context   
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawImage(context, self.bounds, maskRef);          //draw the mask
    CGContextSetBlendMode(context, kCGBlendModeColorBurn);      //set blending mode
    [cachedImage drawAtPoint:CGPointZero];                      //draw the cached image
}

EDIT: After all I combined one of the methods mention below with redrawing only in the new rect. The result is: FAST METHOD:

- (void)addDotAt:(CGPoint)point
{
    if ([myPoints count] < kMaxPoints) {
        Drop *drop = [[[Drop alloc] init] autorelease];
        drop.point = point;
        [myPoints addObject:drop];
        [self setNeedsDisplayInRect:CGRectMake(drop.point.x - drop.dropSize/2, drop.point.y - drop.dropSize/2, drop.dropSize, drop.dropSize)];      //redraw
    }
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextDrawImage(context, self.bounds, maskRef);                                              //draw the mask
    CGContextClipToMask(context, self.bounds, maskRef);                                             //respect alpha mask
    CGContextSetBlendMode(context, kCGBlendModeColorBurn);                                          //set blending mode

    if ([myPoints count] > 0)
    {
        Drop *drop = [myPoints objectAtIndex:[myPoints count]-1];
        CGPathAddEllipseInRect (dotsPath, NULL, CGRectMake(drop.point.x - drop.dropSize/2, drop.point.y - drop.dropSize/2, drop.dropSize, drop.dropSize));
    }
    CGContextAddPath(context, dotsPath);

    CGContextSetRGBFillColor(context, 0.5, 0.0, 0.0, 1.0);
    CGContextFillPath(context);
}

Thanks everyone!

Pomatum answered 25/11, 2009 at 17:6 Comment(0)
O
6

If you are only actually changing a small portion of the UIView's content every time you draw (and the rest of the content generally stays the same), you can use this. Rather than redraw all the content of the UIView every single time, you can mark only the areas of the view that need redrawing using -[UIView setNeedsDisplayInRect:] instead of -[UIView setNeedsDisplay]. You also need to make sure that the graphics content is not cleared before drawing by setting view.clearsContextBeforeDrawing = YES;

Of course, all this also means that your drawRect: implementation needs to respect the rect parameter, which should then be a small subsection of your full view's rect (unless something else dirtied the entire rect), and only draw in that portion.

Ouphe answered 25/11, 2009 at 18:12 Comment(2)
If "view.clearsContextBeforeDrawing = NO;" did what it says in the documentation it should do, all the drawing should be incremental, right? But it isn't...Pomatum
Your drawing isn't incremental unless you make sure that your drawRect: implementation only draws the updated sections. The standard way to mark "updated sections" (dirty sections) is using setNeedsDisplayInRect:. If you set clearsContentBeforeDrawing = NO and then fill the entire context with content anyway, then your performance benefit is small.Ouphe
O
2

You can save your CGPath as a member of your class. And use that in the draw method, you will only need to create the path when the dots change but not every time the view is redraw, if the dots are incremental, just keep adding the ellipses to the path. In the drawRect method you will only need to add the path

CGContextAddPath(context,dotsPath);

-(CGMutablePathRef)createPath
{
    CGMutablePathRef dotsPath =  CGPathCreateMutable();

    for (Drop *drop in myPoints) {
        CGPathAddEllipseInRect ( dotsPath,NULL,
            CGRectMake(drop.point.x - drop.size/2, drop.point.y - drop.size/2, drop.size, drop.size));
    }

return dotsPath;
}
Offish answered 25/11, 2009 at 18:29 Comment(2)
Just tried that too. It might be a bit faster than the other methods but it's still quite slow. I will try to combine it with the drawInRect: method and see if that does it. Thanks.Pomatum
Objective-C classes do not have "members" they have instance variables, or "ivars".Agostino
G
1

If I understand your problem correctly, I would try drawing to a CGBitmapContext instead of the screen directly. Then in the drawRect, draw only the portion of the pre-rendered bitmap that is necessary from the rect parameter.

Galloway answered 25/11, 2009 at 18:33 Comment(2)
I haven't tried anything like this yet. I will probably give it a try. If you have any code that shows how it's done I'd appreciate it.Pomatum
++ That's my method of choice. I just blt the whole thing. It eliminates flashing and gives the illusion of drawing instantaneously.Putumayo
H
0

How many ellipses are you going to draw? In general, Core Graphics should be able to draw a lot of ellipses quickly.

You could, however, cache your old drawings to an image (I don't know if this solution is more performant, however):

UIGraphicsBeginImageContext(self.frame.size);
CGContextRef ctx = UIGraphicsGetCurrentContext(); // ctx is now the image's context

[cachedImage drawAtPoint:CGPointZero];
// only plot new ellipses here...

[cachedImage release];
cachedImage = [UIGraphicsGetImageFromCurrentImageContext() retain];
UIGraphicsEndImageContext();

CGContextRef context = UIGraphicsGetCurrentContext();

CGContextDrawImage(context, self.bounds, maskRef);          //draw the mask
CGContextClipToMask(context, self.bounds, maskRef);         //respect alpha mask
CGContextSetBlendMode(context, kCGBlendModeColorBurn);      //set blending mode

[cachedImage drawAtPoint:CGPointZero];
Halophyte answered 25/11, 2009 at 17:19 Comment(5)
I'm not drawing many, let's say about 100. But with the extra work (drawing an image on every drawRect: AND masking the ellipses on it) it gets veeery slow even on a 3GS.Pomatum
You could also try masking AFTER having painted the ellipses (maybe by first drawing to a buffer, then drawing the buffer maskedly to your view).Halophyte
In the code above, you say "ctx" but mean "context" right? I'm trying your method now to see if it makes a difference.Pomatum
No, your code doesn't work straight. I'll fiddle a bit with it using a UIImage to store my context's contents.Pomatum
I edited my original question to include the implementation using your advice. Unfortunately, it's just as slow...Pomatum
T
0

If you are able to cache the drawing as an image, you can take advantage of UIView's CoreAnimation backing. This will be much faster than using Quartz, as Quartz does its drawing in software.

- (CGImageRef)cachedImage {
    /// Draw to an image, return that
}
- (void)refreshCache {
    myView.layer.contents = [self cachedImage];
}
- (void)actionThatChangesWhatNeedsToBeDrawn {
    [self refreshCache];
}
Toreutic answered 25/11, 2009 at 18:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.