I'm trying to draw some shadows in a rect. The shadow image itself is about 1px * 26px.
Here's two methods I've thought of for drawing the image in the view:
//These methods are called in drawRect:
/* Method 1 */
[self.upperShadow drawInRect:rectHigh]; //upperShadow is UIImage
[self.lowerShadow drawInRect:rectLow];
/* Method 2 */
CALayer *shadowTop = [CALayer layer];
shadowTop.frame = rectHigh;
shadowTop.contents = (__bridge id)topShadow; //topShadow is CGImage
[self.layer addSublayer:shadowTop];
CALayer *shadowLow = [CALayer layer];
shadowLow.frame = rectLow;
shadowLow.contents = (__bridge id)lowShadow;
[self.layer addSublayer:shadowLow];
/* Method 3 */
UIImageView *tShadow = [[UIImageView alloc] initWithFrame:rectHigh];
UIImageView *bShadow = [[UIImageView alloc] initWithFrame:rectLow];
tShadow.image = self.upperShadow;
bShadow.image = self.lowerShadow;
tShadow.contentMode = UIViewContentModeScaleToFill;
bShadow.contentMode = UIViewContentModeScaleToFill;
[self addSubview:tShadow];
[self addSubview:bShadow];
I'm curious which of these is better, when it comes to performance in drawing and animation. From my benchmarking it seems that the layers are faster to draw. Here are some benchmarking stats:
drawInRect: took 0.00054 secs
CALayers took 0.00006 secs
UIImageView took 0.00017 secs
The view which contains these shadows is going to have a view above it which will be animated (the view itself is not). Anything that would degrade the animation performance should be avoided. Any thoughts between the three methods?
If the shadows are static, then the best way is to use two UIImageViews. It's even smarter than CALayer about how to deal with static images (though I don't know if that's going to make a difference here), and will otherwise have the same benefits as CALayer, such as having all compositing being done on the GPU instead of on the CPU (as your Method 2 will require).
Related
I am having a wicked time trying to get an image to show up in a CALayer.contents. It seems rather straight forward but I cannot get the image to render no matter what I do. The CALayer renders fine as I can see its background color and corner radius, but the image will not load.
What you are seeing here is a CAGradientLayer subclass with a mask applied. The inner square is where I would like the image to show, and it is added as a sublayer of the CAGradientLayer subclass.
The code to set this up is pretty straightforward. In init:
self.imageLayer = [CALayer layer];
self.imageLayer.cornerRadius = kDefaultCornerRadius;
self.imageLayer.backgroundColor = [UIColor darkGrayColor].CGColor;
[self addSublayer:self.imageLayer];
Then later on, I set the image:
- (void)setImage:(UIImage *)image {
self.imageLayer.contents = (id)image.CGImage;
[self.imageLayer setNeedsDisplay];
}
Finally, within setFrame:
CGFloat imageSize = self.bounds.size.width - 2*kDefaultMargin;
[self.imageLayer setBounds:CGRectMake(0.0, 0.0, imageSize, imageSize)];
[self.imageLayer setPosition:CGPointMake(self.bounds.size.width/2.0f, kDefaultMargin + imageSize/2.0f)];
[self.imageLayer setNeedsDisplay];
Things I already know or have checked:
The layer is obviously added correctly evident by it being visible.
The image is added and works. [UIImage imageNamed:#"info.png"] is being used elsewhere in the code and actually shows and image. It is 16x16 at 1x and 32x32 at 2x
The code is being called in the correct logical order: init, setImage:, setFrame;
What is going on here?
Remove this line:
[self.imageLayer setNeedsDisplay];
-setNeedsDisplay tells a layer that it needs to redraw its contents. Since you already provided the content for the layer, you don't want CA to discard that content and ask for a replacement.
I have a UIView object that rotates using CALayer's transform:
// Create uiview object.
UIImageView *block = [[UIImageView alloc] initWithFrame....]
// Apply rotation.
CATransform3D basicTrans = CATransform3DIdentity;
basicTrans.m34 = 1.0/-distance;
blockImage.layer.transform = CATransform3DRotate(basicTrans, rangle, 1.0f, 0.0f, 0.0f);
After rotating the edges of the object are not antialiasing. I need to antialias them.
Help me, please. How can it be done?
One way to do this is by placing the image inside another view that's 5 pixels bigger. The bigger view should have a transparent rasterized border that will smooth the edges of the UIImageView:
view.layer.borderWidth = 3;
view.layer.borderColor = [UIColor clearColor].CGColor;
view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = [[UIScreen mainScreen] scale];
Then, place your UIImageView inside this parent view and center it (With 2.5 pixels around each edge).
Finally, rotate the parent view instead of the image view.
It works very well - you can also encapsulate the whole thing in class that creates the hierarchy.
Simply add this key-value pair to your Info.plist: UIViewEdgeAntialiasing set to YES.
check allowsEdgeAntialiasing property of CALayer.
block.layer.allowsEdgeAntialiasing = YES; // iOS7 and above.
I had a similar issue when rotating around the z-axis. Setting shouldRasterize = YES prevented the jagged edges however it came at a performance cost. In my case I was re-using the views (and its layers) and keeping the shouldRasterize = YES was slowing things down.
The solution was, to turn off rasterization right after I didn't need it anymore. However since animation runs on another thread, there was no way of knowing when the animation was complete...until I found out about an extremely useful CATransaction method. This is an actual code that I used and it should illustrate its use:
// Create a key frame animation
CAKeyframeAnimation *wiggle = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
NSInteger frequency = 5; // Higher value for faster vibration
NSInteger amplitude = 25; // Higher value for lower amplitude
// Create the values it will pass through
NSMutableArray *valuesArray = [[NSMutableArray alloc] init];
NSInteger direction = 1;
[valuesArray addObject:#0.0];
for (NSInteger i = frequency; i > 0; i--, direction *= -1) {
[valuesArray addObject:#((direction * M_PI_4 * (CGFloat)i / (CGFloat)amplitude))];
}
[valuesArray addObject:#0.0];
[wiggle setValues:valuesArray];
// Set the duration
[wiggle setAdditive:YES];
[wiggle setValueFunction:[CAValueFunction functionWithName:kCAValueFunctionRotateZ]];
[wiggle setDuration:0.6];
// Turn on rasterization to prevent jagged edges (anti-aliasing issues)
viewToRotate.layer.shouldRasterize = YES;
// ************ Important step **************
// Very usefull method. Block returns after ALL animations have completed.
[CATransaction setCompletionBlock:^{
viewToRotate.layer.shouldRasterize = NO;
}];
// Animate the layer
[viewToRotate.layer addAnimation:wiggle forKey:#"wiggleAnimation"];
worked like a charm for me.
I have not tried using this with implicit animations (i.e. animations that happen due to value change in animatable property for a non-view associated layer), however I would expect it to work as long as the CATransaction method is called before the property change, just as a guarantee the block is given to CATransaction before an animation starts.
I'm using the QuartzImage class in one of the demo projects and what I'm was trying to achieve was a simple frame display unit that basically draws an image (320x480) every 1/10th of sec. So my "frame rate" should be 10 frames per sec.
In the QuartzImage demo class, there is a drawInContext method and in this method, it's basically drawing a CGImageRef using the CGContextDrawImage(), I measured the time it took to finish complete and it's taking on average around ~200ms.
2011-03-24 11:12:33.350 QuartzDemo[3159:207] drawInContext took 0.19105 secs
-(void)drawInContext:(CGContextRef)context
{
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
CGRect imageRect;
imageRect.origin = CGPointMake(0.0, 0.0);
imageRect.size = CGSizeMake(320.0f, 480.0f);
CGContextDrawImage(context, imageRect, image);
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
NSLog(#"drawInContext took %2.5f secs", end - start);
}
Can anyone explain why it's taking that long and if there is any other way of improving the performance? 200ms just seems much more longer than it should have taken.
UPDATES
I tried #Brad-Larson's suggestion but not seeing a lot of performance improvement.
So the updated version is I got my own class
#interface FDisplay : UIView {
CALayer *imgFrame;
NSInteger frameNum;
}
end
So in my Class implementation
- (id)initWithFrame:(CGRect)frame {
............
frameNum = 0;
NSString *file = [NSString stringWithFormat:#"frame%d",frameNum];
UIImage *img = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:file ofType:#"jpg"]];
imgFrame = [CALayer layer];
CGFloat nativeWidth = CGImageGetWidth(img.CGImage);
CGFloat nativeHeight = CGImageGetHeight(img.CGImage);
CGRect startFrame = CGRectMake(0.0, 0.0, nativeWidth, nativeHeight);
imgFrame.contents = (id)img.CGImage;
imgFrame.frame = startFrame;
CALayer *l = [self layer];
[l addSublayer:imgFrame];
}
I have a NSTimer going at 0.1f calling my refresh method
NSString *file = [NSString stringWithFormat:#"frame%d",frameNum];
UIImage *img = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:file ofType:#"jpg"]];
frameNum++;
if (frameNum>100)
frameNum = 0;
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[imgFrame setContents:(id)img.CGImage];
[CATransaction commit];
end = CFAbsoluteTimeGetCurrent();
NSLog(#"(%d)refresh took %2.5f secs", [[self subviews] count],end - start);
I think I got everything right but the frame rate is still way low,
refresh took 0.15960 secs
Using Quartz to draw out images is about the slowest way you could do this, due to the way that content is presented to the screen.
As a step up, you could use a UIImageView and swap out its hosting image for each frame. An even faster approach might be to use a CALayer and set its contents property to a CGImageRef representing each image frame. You may need to disable implicit animations using a CATransaction for this to be as fast as it can be. If I recall, I was able to get 320x480 images to be drawn at over 15 FPS on an original iPhone using the latter method.
Finally, for optimal performance you could set up an OpenGL ES display with a rectangle that filled the screen and supply the images as textures to be rendered on that rectangle. This would require significantly more code, but it would be extremely fast.
I have been porting my game from Android to iPhone and I was shocked by the performance of Quartz.
The same code on Android is 10x faster than on iPhone. I have a few DrawImages, and a bunch of line draws and beziers.
It's so slow on iOS, specially iPhone 4, where the processor struggles to keep up with the retina display resolution.
My game performed perfectly at 60fps in almost ANY android device.
I tried several approaches for rendering (always avoiding OpenGL). I started by drawing everything at every frame. Then I started rendering as much as I could before the game loop, by using UIImage's. Now I'm trying to go with CALayers. Although I can see the game at steady 60FPS on iPhone 3GS and 4S, the most I can get on the iPhone 4 is 45 FPS.
i have a scroll view loaded with 3 view controllers. each view controller is drawing its layers with that code -
(there us more then that but I pulled it out to check if it will help). still i have very crappy sliding.
any help ?
shani
CALayer *sublayer = [CALayer layer];
sublayer.backgroundColor = [Helper cardBackGroundColor:card].CGColor;
sublayer.shadowOffset = CGSizeMake(0, 3);
sublayer.shadowRadius = 5.0;
sublayer.shadowColor = [UIColor blackColor].CGColor;
sublayer.shadowOpacity = 0.8;
sublayer.frame = CGRectInset(self.view.layer.frame, 20, 20);
sublayer.borderColor = [UIColor blackColor].CGColor;
sublayer.borderWidth = 2.0;
sublayer.cornerRadius = 10.0;
[self.view.layer addSublayer:sublayer];
Drawing things with CALayer often yields poor performance. We usually use a stretchable image to get adequate performance. When you think of it, it does make sense to render it before hand rather than using the iPhone's limited processing power to render it in real time.
It's possible that you can get adequate performance from CALayer, but drawing a png will probably still be faster, thus saving battery life time.
EDIT: So here's an example to explain the concept.
This code actually replaced a CALayer drawing that was too slow.
UIImageView *shadow = [[UIImageView alloc] initWithFrame:frame];
shadow.image = [[UIImage imageNamed:#"shadow.png"] stretchableImageWithLeftCapWidth:16.0 topCapHeight:16.0];
[contentView addSubview:shadow];
[shadow release];
shadow.png is 34 by 34 pixels and contains a shadowed square. Thanks to the stretchable image it's possible to resize the square without stretching the shadow. For more information about this I would suggest reading the documentation for stretchableImageWithLeftCapWidth:topCapHeight:. Also Google will help you find guides on how to work with stretchable images. If you have more questions I'll be happy to answer them.
You have a mask (assuming you somewhere say masksToBounds=YES) and a shadow on this layer. Both cause an off screen rendering pass.
Please watch the WWDC 2010 Session 425 - Core Animation in Practice Part 2
Which you can find here;
http://developer.apple.com/videos/wwdc/2010/
I'm running into problems when dealing with a large amount of UIButtons in my interface. I was wondering if anyone had first hand experience with this and how they did it?
When dealing with 30-80 buttons most simple, a couple of complex do you just use UIButton or do something different like drawRect, respond to touch events and get the coordinates of the touch event?
Best example is a calendar, similar to that of Apples Calendar App. Would you just draw most of the days using drawRect and then when you click a button replace it with an image or just use UIButtons? It's not so much the memory footprint or creating the buttons, just strange things are happening with them sometimes (previous question about it) and having performance issues animating them.
Thanks for any help.
If "strange things are happening" with your buttons, you need to get to the bottom of why. Switching architectures just to avoid a problem that you don't understand (and might crop up again) doesn't sound like a good idea.
-drawRect: works by drawing to a bitmap-backed context. This happens when -displayIfNeeded is called after -setNeedsDisplay (or doing something else that implicitly sets the needsDisplay flag, like resizing a view with contentMode = UIContentModeRedraw). The bitmap-backed context is then composited to screen.
Buttons work by putting the different components (background image, foreground image, text) in different layers. The text is drawn when it changes and composited to the screen; the images are just composited directly to the screen.
The "best" way to do things is usually a combination of the two. For example, you might draw text and a background image in -drawRect: so the different layers didn't need to be composited at render time (you get an additional speedup if your view is "opaque"). You probably want to avoid full-screen animations via drawRect: (and it won't integrate so well with CoreAnimation), since drawing tends to be more expensive than compositing.
But first, I'd find out what's going wrong with UIButton. There's little point worrying about how you could make things faster until you actually find out what the slow bits are. Write code so that it is easy to maintain. UIButton is not that expensive and -drawRect: is not that bad (presumably it's even better if you use -setNeedsDisplayInRect: for a smallish rect, but then you need to calculate the rect...), but if you want a button, use UIButton.
Instead of using 30-80 UIButtons I will prefer using images (if possible, a single image or as small number as possible) and compare the touch location.
And if I must create buttons, then obviously will not create 30-80 variables for them. I will set and get view tag to determine which one is tapped.
If this is all stuff you are animating then you could create a bunch of CALayers with their contents set to a CGImage. You would have to compare the touch location to identify the layer. CALayers have a useful style property that is an NSDictionary you can store meta-data in.
I just use the UIButtons unless there happens to be a specific performance issue that crops up. If they have similar functionality, however, such as a keyboard, I map them all to one IBAction and differentiate the behavior based on the sender.
What specific performance and animation issues are you running into?
I recently ran across this problem myself when developing a game for the iPhone. I was using UIButtons to hold game tiles, then stylized them with transparent images, background colors and text.
It all worked well for a small number of tiles. Once we got to about 50, however, the performance dropped significantly. After scouring Google I discovered that others had experienced the same problem. It seems the iPhone struggles with lots of transparent buttons onscreen at once. Not sure if it's a bug in the UIButton code or just a limitation of the graphics hardware on the device, but either way, it's beyond your control as a programmer.
My solution was to draw the board by hand using Core Graphics. It seemed daunting at first, but in reality it was pretty easy. I just placed one big UIImageView on my ViewController in Interface Builder, made it an IBOutlet so I could alter it from Objective-C, then constructed the image with Core Graphics.
Since a UIImageView doesn't handle taps, I used the touchesBegan method of my UIViewController, and then triangulated the x/y coordinates of the touch to the precise tile on my game board.
The board now renders in less than a tenth of a second. Bingo!
If you need sample code, just let me know.
UPDATE: Here's a simplified version of the code I'm using. Should be enough for you to get the gist.
// CoreGraphicsTestViewController.h
// CoreGraphicsTest
#import <UIKit/UIKit.h>
#interface CoreGraphicsTestViewController : UIViewController {
UIImageView *testImageView;
}
#property (retain, nonatomic) IBOutlet UIImageView *testImageView;
-(void) drawTile: (CGContextRef) ctx row: (int) rowNum col: (int) colNum isPressed: (BOOL) tilePressed;
#end
... and the .m file ...
// CoreGraphicsTestViewController.m
// CoreGraphicsTest
#import "CoreGraphicsTestViewController.h"
#import <QuartzCore/QuartzCore.h>
#import <CoreGraphics/CoreGraphics.h>
#implementation CoreGraphicsTestViewController
#synthesize testImageView;
int iTileSize;
int iBoardSize;
- (void)viewDidLoad {
int iRow;
int iCol;
iTileSize = 75;
iBoardSize = 3;
[testImageView setBounds: CGRectMake(0, 0, iBoardSize * iTileSize, iBoardSize * iTileSize)];
CGRect rect = CGRectMake(0.0f, 0.0f, testImageView.bounds.size.width, testImageView.bounds.size.height);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
for (iRow = 0; iRow < iBoardSize; iRow++) {
for (iCol = 0; iCol < iBoardSize; iCol++) {
[self drawTile: context row: iRow col: iCol color: isPressed: NO];
}
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
[testImageView setImage: image];
UIGraphicsEndImageContext();
[super viewDidLoad];
}
- (void)dealloc {
[testImageView release];
[super dealloc];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView: testImageView];
if ((location.x >= 0) && (location.y >= 0) && (location.x <= testImageView.bounds.size.width) && (location.y <= testImageView.bounds.size.height)) {
UIImage *theIMG = testImageView.image;
CGRect rect = CGRectMake(0.0f, 0.0f, testImageView.bounds.size.width, testImageView.bounds.size.height);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[theIMG drawInRect: rect];
iRow = location.y / iTileSize;
iCol = location.x / iTileSize;
[self drawTile: context row: iRow col: iCol color: isPressed: YES];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
[testImageView setImage: image];
UIGraphicsEndImageContext();
}
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UIImage *theIMG = testImageView.image;
CGRect rect = CGRectMake(0.0f, 0.0f, testImageView.bounds.size.width, testImageView.bounds.size.height);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[theIMG drawInRect: rect];
[self drawTile: context row: iRow col: iCol isPressed: NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
[testImageView setImage: image];
UIGraphicsEndImageContext();
}
-(void) drawTile: (CGContextRef) ctx row: (int) rowNum col: (int) colNum isPressed: (BOOL) tilePressed {
CGRect rrect = CGRectMake((colNum * iTileSize), (rowNum * iTileSize), iTileSize, iTileSize);
CGContextClearRect(ctx, rrect);
if (tilePressed) {
CGContextSetFillColorWithColor(ctx, [[UIColor redColor] CGColor]);
} else {
CGContextSetFillColorWithColor(ctx, [[UIColor greenColor] CGColor]);
}
UIImage *theImage = [UIImage imageNamed:#"tile.png"];
[theImage drawInRect: rrect];
}