I am drawing image on a custom UIView. On resizing the view, the drawing performance goes down and it starts lagging.
My image drawing code is below:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *bpath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, width, height)];
CGContextAddPath(context, bpath.CGPath);
CGContextClip(context);
CGContextDrawImage(context, [self bounds], image.CGImage);
}
Is this approach correct?
You would be better using Instruments to find where the bottleneck is than asking on here.
However, what you will probably find is that every time the frame changes slightly the entire view will be redrawn.
If you're just using the drawRect to clip the view into an oval (I guess there's an image behind it or something) then you would be better off using a CAShapeLayer.
Create a CAShapeLayer and give it a CGPath then add it as a clipping layer to the view.layer.
Then you can change the path on the CAShapeLayer and it will update. You'll find (I think) that it performs much better too.
If your height and width are the same, you could just use a UIImageView instead of needing a custom view, and get the circular clipping by setting properties on the image view's layer. That approach draws nice and quickly.
Just set up a UIImageView (called "image" in my example) and then have your view controller do this once:
image.layer.cornerRadius = image.size.width / 2.0;
image.layer.masksToBounds = YES;
Related
I'm trying to zoom and translate an image on the screen.
here's my drawRect:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextSetShouldAntialias(context, NO);
CGContextScaleCTM (context, senderScale, senderScale);
[self.image drawAtPoint:CGPointMake(imgposx, imgposy)];
CGContextRestoreGState(context);
}
When senderScale is 1.0, moving the image (imgposx/imgposy) is very smooth. But if senderScale has any other value, performance takes a big hit and the image stutters when I move it.
The image I am drawing is a UIImageobject. I create it with
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
and draw a simple UIBezierPath(stroke):
self.image = UIGraphicsGetImageFromCurrentImageContext();
Am I doing something wrong? Turning off the anti-aliasing did not improve things much.
Edit:
I tried this:
rectImage = CGRectMake(0, 0, self.frame.size.width * senderScale, self.frame.size.height * senderScale);
[image drawInRect:rectImage];
but it was just as slow as the other method.
If you want this to perform well, you should let the GPU do the heavy lifting by using CoreAnimation instead of drawing the image in your -drawRect: method. Try creating a view and doing:
myView.layer.contents = self.image.CGImage;
Then zoom and translate it by manipulating the UIView relative to its superview. If you draw the image in -drawRect: you're making it do the hard work of blitting the image for every frame. Doing it via CoreAnimation only blits once, and then subsequently lets the GPU zoom and translate the layer.
I have a UIView where I would like to draw a Circle that extends past the frame of the UIView,
I have set the masksToBounds to NO - expecting that I can draw past outside the bounds of the UIView by 5 pixels on the right and bottom.
I expect the oval to not get clipped but it does get clipped and does not draw outside the bounds?
- (void)drawRect:(CGRect)rect
{
int width = self.bounds.size.width;
int height = self.bounds.size.height;
self.layer.masksToBounds = NO;
//// Rounded Rectangle Drawing
//// Oval Drawing
UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(0, 0, width+5, height+5)];
[[UIColor magentaColor] setFill];
[ovalPath fill];
[[UIColor blackColor] setStroke];
ovalPath.lineWidth = 1;
[ovalPath stroke];
}
From http://developer.apple.com/library/ios/#documentation/general/conceptual/Devpedia-CocoaApp/DrawingModel.html
UIView and NSView automatically configure the drawing environment of a
view before its drawRect: method is invoked. (In the AppKit framework,
configuring the drawing environment is called locking focus.) As part
of this configuration, the view class creates a graphics context for
the current drawing environment.
This graphics context is a Quartz object (CGContext) that contains
information the drawing system requires, such as the colors to apply,
the drawing mode (stroke or fill), line width and style information,
font information, and compositing options. (In the AppKit, an object
of the NSGraphicsContext class wraps a CGContext object.) A graphics
context object is associated with a window, bitmap, PDF file, or other
output device and maintains information about the current state of the
drawing environment for that entity. A view draws using a graphics
context associated with the view’s window. For a view, the graphics
context sets the default clipping region to coincide with the view’s
bounds and puts the default drawing origin at the origin of a view’s
boundaries.
Once the clipping region is set, you can only make it smaller. So, what you're trying to do isn't possible in a UIView drawRect:.
I'm not certain this will fix your problem, but it's something to look into. You're setting self.layer.masksToBounds = NO every single time you enter drawRect. You should try setting it inside the init method just once instead, A) because it's unnecessary to do it multiple times and B) because maybe there's a problem with setting it after drawRect has already been called--who knows.
Now i have following:
- (void)drawRect
{
// some drawing
[bgImage drawinRect:self.bounds];
// some drawing
}
I have more than 40 views with text and some marks inside. I need to repaint all these views on user tapping - it should be really fast!
I instrumented my code and saw: 75% of all execution time is [MyView drawRect:] and 95% of my drawRect time is [bgImage drawinRect:self.bounds] call. I need to draw background within GPU nor CPU. How is it possible?
What i have tried:
Using subviews instead of drawRect. In my case it is very slow because of unkickable color blending.
Adding UIImageView as background don't helps, we can't draw on subviews ...
Adding image layer to CALayer? Is it possible?
UPDATE:
Now i am trying to use CALayer instead of drawInRect.
- (void)drawRect
{
// some drawing
CALayer * layer = [CALayer layer];
layer.frame = self.bounds;
layer.contents = (id)image.CGImage;
layer.contentsScale = [UIScreen mainScreen].scale;
layer.contentsCenter = CGRectMake(capInsects.left / image.size.width,
capInsects.top / image.size.height,
(image.size.width - capInsects.left - capInsects.right) / image.size.width,
(image.size.height - capInsects.top - capInsects.bottom) / image.size.height);
// some drawing
}
Nothing instead of this new layer is visible right now. All my painting is under this layer i think...
I would use UILabels; you can reduce the cost of blending by giving the labels a background color or setting shouldRasterize = YES on the layer of the view holding those labels. (You'll probably also want to set rasterizationScale to be the same as [[UIScreen mainScreen] scale]).
Another suggestion:
There are lots of open-source calendar components that have probably had to try and solve the same issues. Perhaps you could look and see how they solved them.
I'm using an app called Quarkee to convert a logo from SVG to Quartz 2d code, which works a treat. Only problem is I can't seem to figure out how resize the result. If I set the frame of the UIView, the result from drawRect stays huge in the frame. How do I get it be the size of the frame I'm setting?
An example of the out is below.
Can someone help?
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorSpaceRef colorspace_1 = CGColorSpaceCreateDeviceRGB();
CGFloat components_1[] = {0.9961,0.9961,0.9961, 1.0000};
CGColorRef color_1 = CGColorCreate(colorspace_1, components_1);
CGContextSetFillColorWithColor(context,color_1);
CGContextMoveToPoint(context, 0.4960,0.1090);
CGContextAddLineToPoint(context,662.9260,0.1090);
CGContextAddLineToPoint(context,662.9260,227.8780);
CGContextAddLineToPoint(context,0.4960,227.8780);
CGContextAddLineToPoint(context,0.4960,0.1090);
CGContextClosePath(context);
CGContextFillPath(context);
The solution here was to use view.transform = CGAffineTransformMakeScale(0.5f, 0.5f); to resize the view
Take a look at these values:
CGContextMoveToPoint(context, 0.4960,0.1090);
CGContextAddLineToPoint(context,662.9260,0.1090);
CGContextAddLineToPoint(context,662.9260,227.8780);
CGContextAddLineToPoint(context,0.4960,227.8780);
CGContextAddLineToPoint(context,0.4960,0.1090);
when the drawRect method is called you can use self.bounds to take the current rect of your view and adjust those values according to it. You say you have generated this code from a application - those apps usually hardcode the size of the graphics you have drawn, when generate code, so you must see what is the relation between these values with the actual size of the image you have drawn and make them dynamic according to the self.bounds size...
I'm creating an app that allows users to cut out part of an image. In order to do this, they'll create a bunch of UIBezierPaths to form the clipping path. My current setup is as follows:
A UIImageView displays the image they're cutting.
Above that UIImageView is a custom subclass of UIImageView that
performs the custom drawRect: methods for showing/updating the
UIBezierPaths that the user is adding.
When the user clicks the "Done" button, a new UIBezierPath object is created that incorporates all the individual paths created by the user by looping through the array they're stored in and calling appendPath: on itself. This new UIBezierPath then closes its path.
That's as far as I've gotten. I know UIBezierPath has an addClip method, but I can't figure out from the documentation how to use it.
In general, all examples I've seen for clipping directly use Core Graphics rather than the UIBezierPath wrapper. I realize that UIBezierPath has a CGPath property. So should I be using this at the time of clipping rather than the full UIBezierPath object?
Apple say not to subclass UIImageView, according to the UIImageView class reference. Thank you to #rob mayoff for pointing this out.
However, if you're implementing your own drawRect, start with your own UIView subclass. And, it's within drawRect that you use addClip. You can do this with a UIBezierPath without converting it to a CGPath.
- (void)drawRect:(CGRect)rect
{
// This assumes the clippingPath and image may be drawn in the current coordinate space.
[[self clippingPath] addClip];
[[self image] drawAtPoint:CGPointZero];
}
If you want to scale up or down to fill the bounds, you need to scale the graphics context. (You could also apply a CGAffineTransform to the clippingPath, but that is permanent, so you'd need to copy the clippingPath first.)
- (void)drawRect:(CGRect)rect
{
// This assumes the clippingPath and image are in the same coordinate space, and scales both to fill the view bounds.
if ([self image])
{
CGSize imageSize = [[self image] size];
CGRect bounds = [self bounds];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextScaleCTM(context, bounds.size.width/imageSize.width, bounds.size.height/imageSize.height);
[[self clippingPath] addClip];
[[self image] drawAtPoint:CGPointZero];
}
}
This will scale the image separately on each axis. If you want to preserve its aspect ratio, you'll need to work out the overall scaling, and possibly translate it so it's centered or otherwise aligned.
Finally, all of this is relatively slow if your path gets drawn a lot. You will probably find it's faster to store the image in a CALayer, and mask that with a CAShapeLayer containing the path. Do not use the following methods except for testing. You will need to separately scale the image layer and the mask to make them line up. The advantage is that you can change the mask without the underlying image being rendered.
- (void) setImage:(UIImage *)image;
{
// This method should also store the image for later retrieval.
// Putting an image directly into a CALayer will stretch the image to fill the layer.
[[self layer] setContents:(id) [image CGImage]];
}
- (void) setClippingPath:(UIBezierPath *)clippingPath;
{
// This method should also store the clippingPath for later retrieval.
if (![[self layer] mask])
[[self layer] setMask:[CAShapeLayer layer]];
[(CAShapeLayer*) [[self layer] mask] setPath:[clippingPath CGPath]];
}
If you do make image clipping with layer masks work, you no longer need a drawRect method. Remove it for efficiency.