I need to draw many lines (in the range of 50-75) in a screen and currently use the below function for it which works fine. After drawing 40-50 of these lines with the below code, the application slows down noticeably in my iPhone 4. To optimize I tried removing the line shadow it helped but still app wasn't running as smooth as I wanted. I need to optimize the below code, my first idea is to replace the cashapelayers with .png line images. But the new method should support line rotation, different length line with same width, and animation of drawing (it seems a lot for me to do with cgaffinetransforms). Any ideas that can help me?
+ (CAShapeLayer *) drawLineOnView:(UIView *) view BetweenPoint1:(CGPoint) point1 Point2:(CGPoint) point2 lineWidth:(CGFloat)lineWidth lineColor:(UIColor *) color Animated:(BOOL) animed
{
CAShapeLayer *lineShape = [CAShapeLayer layer];
CGMutablePathRef linePath = nil;
linePath = CGPathCreateMutable();
//lineShape.opacity = 0.6;
lineShape.lineWidth = lineWidth;
lineShape.lineCap = kCALineCapRound;
if(color==nil) color = [UIColor orangeColor]; //Default value
lineShape.shadowColor = [color CGColor];
lineShape.shadowOpacity = 1.0;
lineShape.shadowRadius = 5.0;
lineShape.strokeColor = [color CGColor];
CGPathMoveToPoint(linePath, NULL, point1.x, point1.y);
CGPathAddLineToPoint(linePath, NULL, point2.x, point2.y);
if(animed)
{
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 1.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[lineShape addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
lineShape.path = linePath;
CGPathRelease(linePath);
[view.layer addSublayer:lineShape];
return lineShape;
}
PARTLY SOLVED (Optimization never ends)
I broke down my line drawing function into 2 complementary parts and draw multiple lines into the one shape layer instead of creating new layers each time. It works much better if not great. Here is the updated code:
+ (CAShapeLayer *) createNewShapeLayerForDrawingLinesOnView:(UIView *) view lineWidth:(CGFloat)lineWidth lineColor:(UIColor *) color
{
CAShapeLayer *lineShape = [CAShapeLayer layer];
//lineShape.opacity = 0.6;
lineShape.lineWidth = lineWidth;
lineShape.lineCap = kCALineCapRound;
if(color==nil) color = [UIColor orangeColor]; //Default value
lineShape.shadowColor = [color CGColor];
lineShape.shadowOpacity = 1.0;
lineShape.shadowRadius = 5.0;
lineShape.strokeColor = [color CGColor];
[view.layer addSublayer:lineShape];
return lineShape;
}
+ (void) addNewLineToShapeLayer:(CAShapeLayer *) shapeLayer BetweenPoint1:(CGPoint) point1 Point2:(CGPoint) point2
{
CGMutablePathRef combinedPath = CGPathCreateMutableCopy(shapeLayer.path);
CGMutablePathRef linePath = CGPathCreateMutable();
CGPathMoveToPoint(linePath, NULL, point1.x, point1.y);
CGPathAddLineToPoint(linePath, NULL, point2.x, point2.y);
//No paths drawn before
if(combinedPath == NULL)
{
combinedPath = linePath;
}
else
{
CGPathAddPath(combinedPath, NULL, linePath);
}
shapeLayer.path = combinedPath;
CGPathRelease(linePath);
}
While I understand the want to create multiple layers, it will be much more efficient to draw all the lines into one and to manage animations and rotations of a list of lines from there. You can do this in a shape layer with a combined path like(missing code marked with "..."):
CGMutablePathRef combinedPath = CGPathCreateMutableCopy(path.CGPath);
for(...)
CGPathAddPath(combinedPath, NULL, [self makeNewPathFrom:...].CGPath);
myLayer.path = combinedPath;
Even faster, you can draw the list of lines directly onto the graphics context of a CALayer. This example for a view's drawRect: method is untested but should give you an idea:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, lineWidth);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0); //red
for(MyLine *line in lines)
{
CGContextMoveToPoint(context, point1.x, point1.y);
CGContextAddLineToPoint(context, point2.x, point2.y);
}
If you need further optimization, you should look into OpenGL.
You definitely do not want 75 layers, each with their own line. Are you sure you can't draw a single, more complex, path in a single layer?
Related
I'm creating a simple app where when the user presses a button, a series of lines will be drawn on the screen and the user will be able to see these lines drawn in real time (almost like an animation).
My code looks something like this (has been simplified):
UIGraphicsBeginImageContext(CGSizeMake(300,300));
CGContextRef context = UIGraphicsGetCurrentContext();
for (int i = 0; i < 100; i++ ) {
CGContextMoveToPoint(context, i, i);
CGContextAddLineToPoint(context, i+20, i+20);
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
CGContextStrokePath(context);
}
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
My problem is that:
1) As soon as the user presses the button, the UIThread blocks until the drawing is done.
2) I can't get the lines to be drawn on the screen one at a time - I've tried setting the UIImage directly inside the loop and also tried setting a layer content inside the loop.
How do I get around these problems?
You say "just like an animation". Why not do an actual animation, a la Core Graphics' CABasicAnimation? Do you really need to show it as a series of lines, or is a proper animation ok?
If you want to animate the actual drawing of the line, you could do something like:
#import <QuartzCore/QuartzCore.h>
- (void)drawBezierAnimate:(BOOL)animate
{
UIBezierPath *bezierPath = [self bezierPath];
CAShapeLayer *bezier = [[CAShapeLayer alloc] init];
bezier.path = bezierPath.CGPath;
bezier.strokeColor = [UIColor blueColor].CGColor;
bezier.fillColor = [UIColor clearColor].CGColor;
bezier.lineWidth = 5.0;
bezier.strokeStart = 0.0;
bezier.strokeEnd = 1.0;
[self.view.layer addSublayer:bezier];
if (animate)
{
CABasicAnimation *animateStrokeEnd = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animateStrokeEnd.duration = 10.0;
animateStrokeEnd.fromValue = [NSNumber numberWithFloat:0.0f];
animateStrokeEnd.toValue = [NSNumber numberWithFloat:1.0f];
[bezier addAnimation:animateStrokeEnd forKey:#"strokeEndAnimation"];
}
}
Then all you have to do is create the UIBezierPath for your line, e.g.:
- (UIBezierPath *)bezierPath
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0.0, 0.0)];
[path addLineToPoint:CGPointMake(200.0, 200.0)];
return path;
}
If you want, you can patch a bunch of lines together into a single path, e.g. here is a roughly sine curve shaped series of lines:
- (UIBezierPath *)bezierPath
{
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint point = self.view.center;
[path moveToPoint:CGPointMake(0, self.view.frame.size.height / 2.0)];
for (CGFloat f = 0.0; f < M_PI * 2; f += 0.75)
{
point = CGPointMake(f / (M_PI * 2) * self.view.frame.size.width, sinf(f) * 200.0 + self.view.frame.size.height / 2.0);
[path addLineToPoint:point];
}
return path;
}
And these don't block the main thread.
By the way, you'll obviously have to add the CoreGraphics.framework to your target's Build Settings under Link Binary With Libraries.
I am trying to draw an image like below with libraries in iOS; but i couldn't.
I think it is very easy to draw but i couldn't achieve.
After i accomplish to draw i will place a label over it.
Use this as your drawRect method:
- (void)drawRect:(CGRect)rect
//// General Declarations
CGContextRef context = UIGraphicsGetCurrentContext();
//// Shadow Declarations
UIColor* shadow = [UIColor blackColor];
CGSize shadowOffset = CGSizeMake(1, 1);
CGFloat shadowBlurRadius = 2;
//// Frames
CGRect frame = rect;
//// Abstracted Graphic Attributes
CGRect shadowBoxRect = CGRectMake(CGRectGetMinX(frame) + 0, CGRectGetMinY(frame) + 0, 40, 40);
CGFloat shadowBoxCornerRadius = 4;
//// ShadowBox Drawing
UIBezierPath* shadowBoxPath = [UIBezierPath bezierPathWithRoundedRect: shadowBoxRect cornerRadius: shadowBoxCornerRadius];
[[UIColor lightGrayColor] setFill];
[shadowBoxPath fill];
////// ShadowBox Inner Shadow
CGRect shadowBoxBorderRect = CGRectInset([shadowBoxPath bounds], -shadowBlurRadius, -shadowBlurRadius);
shadowBoxBorderRect = CGRectOffset(shadowBoxBorderRect, -shadowOffset.width, -shadowOffset.height);
shadowBoxBorderRect = CGRectInset(CGRectUnion(shadowBoxBorderRect, [shadowBoxPath bounds]), -1, -1);
UIBezierPath* shadowBoxNegativePath = [UIBezierPath bezierPathWithRect: shadowBoxBorderRect];
[shadowBoxNegativePath appendPath: shadowBoxPath];
shadowBoxNegativePath.usesEvenOddFillRule = YES;
CGContextSaveGState(context);
{
CGFloat xOffset = shadowOffset.width + round(shadowBoxBorderRect.size.width);
CGFloat yOffset = shadowOffset.height;
CGContextSetShadowWithColor(context,
CGSizeMake(xOffset + copysign(0.1, xOffset), yOffset + copysign(0.1, yOffset)),
shadowBlurRadius,
shadow.CGColor);
[shadowBoxPath addClip];
CGAffineTransform transform = CGAffineTransformMakeTranslation(-round(shadowBoxBorderRect.size.width), 0);
[shadowBoxNegativePath applyTransform: transform];
[[UIColor grayColor] setFill];
[shadowBoxNegativePath fill];
}
CGContextRestoreGState(context);
}
Inner shadows are hard to do with CoreGraphics -- basically, you need to negate your path and draw a drop shadow below it, clipped to your original path.
You can take a look at PaintCode and it will show you the code. It has a 15-min demo mode if you don't want to purchase it, that should be enough for your needs.
You could try this:
#import <QuartzCore/QuartzCore.h>
and in your code , after making the your view set these:
self.layer.cornerRadius = x;
self.layer.masksToBounds = TRUE;
This allows you to have rounded corners on your view. And if you calculate the radius to match your view , you should get the desired look.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor grayColor];
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context =UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
// And draw with a blue fill color
CGContextSetRGBFillColor(context, 0.0, 0.0, 1.0, 1.0);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 2.0);
CGContextAddRect(context, self.bounds);
CGContextStrokePath(context);
// Close the path
CGContextClosePath(context);
// Fill & stroke the path
CGContextDrawPath(context, kCGPathFillStroke);
self.layer.cornerRadius = self.bounds.size.width/12;
self.layer.masksToBounds = TRUE;
}
I think it will be helpful to you.
Try the below code where myView is the UIView to which you want to set the shadow.
myView.layer.cornerRadius = 5.0f;
myView.layer.masksToBounds = YES;
[myView.layer setShadowColor:[[UIColor blackColor] colorWithAlphaComponent: 0.5]];
[myView.layer setShadowOffset:CGSizeMake(0, -1)];
Hope this helps.-
I am trying to do a custom tab bar with a transparent triangle that points into the tab bar's view.
Right now I am drawing a linear gradient and a gloss in the drawRect method for the background of this tab bar. I just need to add the transparent triangle on there. I know how to draw a triangle. I just need to know how to make it transparent to show the background beneath the tab bar view.
Anyone know how to do this?
Update
Here is the current code:
void drawGlossAndGradient(CGContextRef context, CGRect rect, CGColorRef startColor, CGColorRef endColor)
{
drawLinearGradient(context, rect, startColor, endColor);
CGColorRef glossColor1 = [UIColor colorWithRed:1.0 green:1.0
blue:1.0 alpha:0.35].CGColor;
CGColorRef glossColor2 = [UIColor colorWithRed:1.0 green:1.0
blue:1.0 alpha:0.1].CGColor;
CGRect topHalf = CGRectMake(rect.origin.x, rect.origin.y,
rect.size.width, rect.size.height/2);
drawLinearGradient(context, topHalf, glossColor1, glossColor2);
}
void drawLinearGradient(CGContextRef context, CGRect rect, CGColorRef startColor, CGColorRef endColor)
{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat locations[] = { 0.0, 1.0 };
NSArray *colors = [NSArray arrayWithObjects:(__bridge id)startColor, (__bridge id)endColor, nil];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGContextSaveGState(context);
CGContextAddRect(context, rect);
CGContextClip(context);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(context);
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
}
- (void)drawTriangle
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint pt1 = CGPointMake(0.0f, 0.0f);
CGPoint pt2 = CGPointMake(10.0f, 10.0f);
CGPoint pt3 = CGPointMake(20.0f, 0.0f);
CGPoint vertices[] = {pt1, pt2, pt3, pt1};
CGContextBeginPath(context);
CGContextAddLines(context, vertices, 3);
CGContextClosePath(context);
CGContextClip(context);
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef lightColor = [UIColor colorWithRed:65.0f/255.0f green:64.0f/255.0f
blue:66.0f/255.0f alpha:1.0].CGColor;
CGColorRef darkColor = [UIColor colorWithRed:37.0/255.0 green:31.0/255.0
blue:32.0/255.0 alpha:1.0].CGColor;
[self drawTriangle];
CGRect viewRect = self.bounds;
drawGlossAndGradient(context, viewRect, lightColor, darkColor);
}
I added the clip suggested below but that just made my background with the gradient and the gloss dissappear and the triangle become gray. Does anyone know what I am doing wrong here?
If you draw this gradient in drawRect: method just add clipping path before it.
Example:
-(void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGPoint vertices[] = {coordinates of vertices};
CGContextBeginPath(ctx);
CGContextAddLines(ctx, vertices, sizeof(vertices)/sizeof(CGPoint));
CGContextClosePath(ctx);
CGContextClip(ctx);
// draw the gradient
}
Vertices - is an array with 7 points. 1 point per each corner of self.bounds and 3 points which are define the triangle.
For example:
(0) (1) (3) (4)
_____ ________________
| \ / |
| V |
| (2) |
(6) |_______________________|(5)
CGPoint vertices[7] = {CGPointZero, // origin
p1, p2, p3, // vertices of the triangle
{self.bounds.size.width, 0},
{self.bounds.size.width, self.bounds.size.height},
{0, self.bounds.size.height}
}
If anybody needs, this is a code to draw a colored triangle in a UITableView without using UIImageView. This method is good because, the size of triangle can vary and it is drawn programmatically. Also, you can probably change colors and make triangle transparent.
#implementation RRTriangleView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0.0, 0.0);
CGPathAddLineToPoint(path, NULL, - self.bounds.size.height / 2.0, self.bounds.size.height / 2.0);
CGPathAddLineToPoint(path, NULL, 0.0, self.bounds.size.height);
CGPathAddLineToPoint(path, NULL, 0.0f, 0.0f);
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
[shapeLayer setPath:path];
[shapeLayer setFillColor:[COLOR_CUSTOM_LIGHT_BLUE CGColor]];
[shapeLayer setStrokeColor:[COLOR_CUSTOM_LIGHT_BLUE CGColor]];
[shapeLayer setPosition:CGPointMake(self.bounds.size.width, 0.0f)];
[[self layer] addSublayer:shapeLayer];
CGPathRelease(path);
}
return self;
}
Init this TriangleView and add it to your cell, when it is selected :
- (RRTriangleView *)triangleView
{
if (! _triangleView) {
_triangleView = [[RRTriangleView alloc] initWithFrame:self.bounds];
_triangleView.layer.backgroundColor = [[UIColor clearColor] CGColor];
_triangleView.clipsToBounds = NO;
}
return _triangleView;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
//[super setSelected:selected animated:animated];
if (selected) {
[self addSubview:self.triangleView];
}
else {
[self.triangleView removeFromSuperview];
}
}
The size of triangleView is like your cell's view, it is transparent and it is drawn above.
I beleive that
[view setBackgroundColor:[UIColor clearColor]];
[view setOpaque:NO];
will make your view transparent.
I've spent too much time trying to figure this out and simply cannot find a workable solution.
Situation:
1. A picture of 'something' is displayed on the phone.
2. A semi-transparent (e.g. blue) layer is placed on top of the image, completely covering it.
3. A 'hole' in this layer exists where that part of the layer is fully transparent and is movable.
An example could be a zoom effect where you move this 'hole' around the image. Inside the hole you can see the image normally, while outside it's covered by the semi-transparent layer. NOTE: I'm implementing this in a cocos2d layer, where the image is represented by a CCSprite. It shouldn't matter though, if no cocos is used.
Problem:
I've tried using CAShapeLayer and bitmaps as masks, but nothing is working (see code snippets below). With the CAShapeLayer, I create a UIBezierPath for the 'hole' and apply it to the colored layer. However, only the hole shows the color, while the rest is transparent. With an image, the mask is simply not working (I have no idea why). I've even tried masking masks to see if that would work. I've also tried swapping colors around...from white to black to clear for fill and background.
A simple solution, if it existed, would be to invert the area of the UIBezierPath. I've tried clipping, as well, using the path...but no luck.
I'm hoping that it's something simple-stupid that I'm simply overlooking. Perhaps one of you will see this. The moving part I'm not, yet, concerned with. I need to get the actual mask working first. The sample code is ignoring the y-axis differences between iPhone SDK and openGL.
CAShapeLayer Example:
CGSize winSize = [[CCDirector sharedDirector] winSize];
UIImage* img = [UIImage imageNamed:#"zebra.png"];
CCSprite* spr = [CCSprite spriteWithCGImage:img.CGImage key:#"img"];
spr.position = ccp( winSize.width / 2, winSize.width / 2 );
[self addSprite:spr];
UIBezierPath* path = [UIBezierPath bezierPathWithRect:rectHole];
CAShapeLayer* maskLayer = [CAShapeLayer layer];
maskLayer.bounds = [spr boundingBox];
maskLayer.position = spr.position;
maskLayer.fillColor = [UIColor whiteColor].CGColor;
maskLayer.backgroundColor = [UIColor clearColor].CGColor;
maskLayer.path = path.CGPath;
CALayer* colorLayer = [CALayer layer];
colorLayer.bounds = [spr boundingBox];
colorLayer.position = maskLayer.position;
[colorLayer setMask:maskLayer];
[[[[CCDirector sharedDirector] openGLView] layer] addSublayer:colorLayer];
Multiple Layer Mask Example:
CGSize winSize = [[CCDirector sharedDirector] winSize];
UIImage* img = [UIImage imageNamed:#"zebra.png"];
CCSprite* spr = [CCSprite spriteWithCGImage:img.CGImage key:#"img"];
spr.position = ccp( winSize.width / 2, winSize.width / 2 );
[self addSprite:spr];
UIBezierPath* path = [UIBezierPath bezierPathWithRect:rectHole];
CAShapeLayer* maskLayer = [CAShapeLayer layer];
maskLayer.bounds = [spr boundingBox];
maskLayer.position = spr.position;
maskLayer.fillColor = [UIColor whiteColor].CGColor;
maskLayer.backgroundColor = [UIColor clearColor].CGColor;
maskLayer.path = path.CGPath;
UIBezierPath* pathOuter = [UIBezierPath bezierPathWithRect:img.frame];
CAShapeLayer* outerLayer = [CAShapeLayer layer];
outerLayer.bounds = [spr boundingBox];
outerLayer.position = spr.position;
outerLayer.fillColor = [UIColor blackColor].CGColor;
outerLayer.backgroundColor = [UIColor whiteColor].CGColor;
outerLayer = pathOuter.CGPath;
[outerLayer setMask:maskLayer];
CALayer* colorLayer = [CALayer layer];
colorLayer.bounds = [spr boundingBox];
colorLayer.position = outerLayer.position;
[colorLayer setMask:outerLayer];
[[[[CCDirector sharedDirector] openGLView] layer] addSublayer:colorLayer];
Image Mask Example:
CGSize winSize = [[CCDirector sharedDirector] winSize];
UIImage* img = [UIImage imageNamed:#"zebra.png"];
CCSprite* spr = [CCSprite spriteWithCGImage:img.CGImage key:#"img"];
spr.position = ccp( winSize.width / 2, winSize.width / 2 );
[self addSprite:spr];
CGRect r = [spr boundingBox];
CGSize sz = CGSizeMake( r.size.width, r.size.height );
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
CGContextRef context = CGBitmapContextCreate( NULL, w, h, 8, 0, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaNone );
CGColorSpaceRelease( colorSpace );
CGContextSetFillColorWithColor( context, [UIColor whiteColor].CGColor );
CGContextFillRect( context, r );
CGContextSetFillColorWithColor( context, [UIColor blackColor].CGColor );
CGContextFillRect( context, rectHole );
CGImageRef ref = CGBitmapContextCreateImage( context );
CGContextRelease( context );
CALayer* maskLayer = [CALayer layer];
maskLayer.bounds = [spr boundingBox];
maskLayer.position = spr.position;
[maskLayer setContents:(id)ref];
CALayer* colorLayer = [CALayer layer];
colorLayer.bounds = [spr boundingBox];
colorLayer.position = maskLayer.position;
[colorLayer setMask:maskLayer];
[[[[CCDirector sharedDirector] openGLView] layer] addSublayer:colorLayer];
CGImageRelease( ref );
I came back to this later after learning other core graphics techniques.
The solution is closest to the Multiple Layer Mask Example above.
However, instead of creating an inner and outer layer, you need to combine two paths into a single UIBezierPath in opposite directions.
So, e.g., create a path of the inner area to be cropped (CW). NOTE: x,y,w,h are referring to the origin and size of the "hole".
[path moveToPoint:ccp(x,y)];
[path addLineToPoint:ccp(x+w,y)];
[path addLineToPoint:ccp(x+w,y+h)];
[path addLineToPoint:ccp(x,y+h)];
[path addLineToPoint:ccp(x,y)];
Then, add to the same path the outer area in the opposite direction (CCW). NOTE: x,y,w,h are referring to the origin and size of the outer rect.
[path moveToPoint:ccp(x,y)];
[path addLineToPoint:ccp(x,y+h)];
[path addLineToPoint:ccp(x+w,y+h)];
[path addLineToPoint:ccp(x+w,y)];
[path addLineToPoint:ccp(x,y)];
This path is then applied to a layer (maskLayer), which is used as the mask on the final layer (colorLayer). The "outerLayer" is not needed.
I'm writing an app that plots some data into a simple graph and sometimes want to draw out the scale in the background. To do this I have a UIView subclass which acts as the graph background and simply use the drawRect: method to draw the scale (data elements will get added as subviews to this view, since I want the user to be able to interact with them).
However, I also want a gradient background color and have used CAGradientLayer for this purpose (as suggested in this thread). But when I add this as a sublayer, the gradient background appears, but nothing I do in drawRect: shows!
I'm sure I'm missing something simple or have misunderstood how to use CAGradientLayer or something, so any help or suggestions is appreciated!
This is the relevant code in my UIView subclass:
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// Create a gradient background
CAGradientLayer *gradientBackground = [CAGradientLayer layer];
gradientBackground.frame = self.bounds;
gradientBackground.colors = [NSArray arrayWithObjects:(id)[[UIColor grayColor] CGColor], (id)[[UIColor whiteColor] CGColor], nil];
[self.layer insertSublayer:gradientBackground atIndex:0];
}
return self;
}
- (void)drawRect:(CGRect)rect {
// Get the graphics context
CGContextRef context = UIGraphicsGetCurrentContext();
// Clear the context
CGContextClearRect(context, rect);
// Call actual draw method
[self drawInContext:context];
}
-(void)drawInContext:(CGContextRef)context {
CGFloat step;
// Draw Y scale
if (displayYScale) {
CGContextSetRGBStrokeColor(context, 0, 0, 0, kScaleLineAlpha);
if (yAxisScale.mode == FCGraphScaleModeData) {
step = (self.frame.size.height-(yAxisScale.padding*2))/yAxisScale.dataRange.maximum;
for (int i = 0; i <= yAxisScale.dataRange.maximum; i++) {
CGContextMoveToPoint(context, 0.0f, (step*i)+yAxisScale.padding);
CGContextAddLineToPoint(context, self.frame.size.width, (step*i)+yAxisScale.padding);
}
} else if (yAxisScale.mode == FCGraphScaleModeDates) {
int units = (int)[yAxisScale units];
step = (self.frame.size.height-(yAxisScale.padding*2))/units;
for (int i = 0; i <= units; i++) {
CGContextMoveToPoint(context, 0.0f, (step*i)+yAxisScale.padding);
CGContextAddLineToPoint(context, self.frame.size.width, (step*i)+yAxisScale.padding);
}
}
CGContextStrokePath(context);
}
// Draw X scale
if (displayXScale) {
CGContextSetRGBStrokeColor(context, 0, 0, 255, kScaleLineAlpha);
if (xAxisScale.mode == FCGraphScaleModeData) {
step = (self.frame.size.width-(xAxisScale.padding*2))/xAxisScale.dataRange.maximum;
for (int i = 0; i <= xAxisScale.dataRange.maximum; i++) {
CGContextMoveToPoint(context, (step*i)+xAxisScale.padding, 0.0f);
CGContextAddLineToPoint(context, (step*i)+xAxisScale.padding, self.frame.size.height);
}
} else if (xAxisScale.mode == FCGraphScaleModeDates) {
int units = (int)[xAxisScale units];
step = (self.frame.size.width-(xAxisScale.padding*2))/units;
for (int i = 0; i <= units; i++) {
CGContextMoveToPoint(context, (step*i)+xAxisScale.padding, 0.0f);
CGContextAddLineToPoint(context, (step*i)+xAxisScale.padding, self.frame.size.height);
}
}
CGContextStrokePath(context);
}
}
Thanks!
/Anders
Since you're drawing anyway, you can also just draw your own gradient:
// the colors
CGColorRef topColor = [[UIColor grayColor] CGColor];
CGColorRef bottomColor = [[UIColor whiteColor] CGColor];
NSArray *colors =
[NSArray arrayWithObjects: (id)topColor, (id)bottomColor, nil];
CGFloat locations[] = {0, 1};
CGGradientRef gradient =
CGGradientCreateWithColors(CGColorGetColorSpace(topColor),
(CFArrayRef)colors, locations);
// the start/end points
CGRect bounds = self.bounds;
CGPoint top = CGPointMake(CGRectGetMidX(bounds), bounds.origin.y);
CGPoint bottom = CGPointMake(CGRectGetMidX(bounds), CGRectGetMaxY(bounds));
// draw
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawLinearGradient(context, gradient, top, bottom, 0);
CGGradientRelease(gradient);
Sublayers will show above anything done in drawRect. Add another sublayer on top of the gradient layer that uses your view as the delegate, in that layers drawLayer:inContext: callback do what you're currently doing in drawRect.