How do I cut a notch out of a CALayer? - iphone

I have a set of views in a carousel, each using a CAGradientLayer as a background. The carousel sits over a textured background. I've been asked for the background to poke up in a triangle to show the selected view. I can't just use a triangular image with the background texture, as it won't necessarily match up with the main background. I'd like to cut a notch out of the background of the current view, so that the textured background is visible through the notch.
How should I go about this? Is it possible to make a polygonal layer?

I found I was able to do it using a CAShapeLayer:
CAShapeLayer *mask = [[[CAShapeLayer alloc] init] autorelease];
mask.frame = backgroundLayer.bounds;
mask.fillColor = [[UIColor blackColor] CGColor];
CGFloat width = backgroundLayer.frame.size.width;
CGFloat height = backgroundLayer.frame.size.height;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, 0, 0);
CGPathAddLineToPoint(path, nil, width, 0);
CGPathAddLineToPoint(path, nil, width, height);
CGPathAddLineToPoint(path, nil, (width/2) + 5, height);
CGPathAddLineToPoint(path, nil, width/2, height - 5);
CGPathAddLineToPoint(path, nil, (width/2) - 5, height);
CGPathAddLineToPoint(path, nil, 0, height);
CGPathAddLineToPoint(path, nil, 0, 0);
CGPathCloseSubpath(path);
mask.path = path;
CGPathRelease(path);
backgroundLayer.mask = mask;

I don't think that we can draw polygonal layers. However, I believe that the expected result can be achieved using two different ways:-
If your images are of the same size then you can use a PNG image with transparent notch in between (or outside as per your desire).
Draw a filled rectangle and transparent triangular polygon. Then you need to intersect both the polygon (rectangle and triangle). The resulted Shape should be placed over the carousel image. The benefit of this method is that you can dynamically change the size and shapes of the polygons if required.
Hope this helps!

Is it possible to make a polygonal layer?
No.
How should I go about this?
Use a mask - a png image that's the same size as your layer but has a bit cut out - this will make the CALayer appear to have a chunk cut out of it. (However, it won't so if you cut-out is big, you might get users trying to touch whatever is behind it, which won't work!).
See the documentation (and search stackoverflow) for more details.

Related

How can I draw a curved shadow?

Like so:
I know that this will not work with NSShadow, drawing it in drawRect: will work just fine.
You can do this and many other kinds of shadows using Core Animations layers and the shadowPath property. The shadow that you are describing can be make with an elliptical shadow path.
The code to produce this shadow is below. You can tweak the size of the ellipse to have a rounder shape of the shadow. You can also tweak the position, opacity, color and blur radius using the shadow properties on the layer.
self.wantsLayer = YES;
NSView *viewWithRoundShadow = [[NSView alloc] initWithFrame:NSMakeRect(30, 30, 200, 100)];
[self addSubview:viewWithRoundShadow];
CALayer *backingLayer = viewWithRoundShadow.layer;
backingLayer.backgroundColor = [NSColor orangeColor].CGColor;
// Configure shadow
backingLayer.shadowColor = [NSColor blackColor].CGColor;
backingLayer.shadowOffset = CGSizeMake(0, -1.);
backingLayer.shadowRadius = 5.0;
backingLayer.shadowOpacity = 0.75;
CGRect shadowRect = backingLayer.bounds;
CGFloat shadowRectHeight = 25.;
shadowRect.size.height = shadowRectHeight;
// make narrow
shadowRect = CGRectInset(shadowRect, 5, 0);
backingLayer.shadowPath = CGPathCreateWithEllipseInRect(shadowRect, NULL);
Just to show some examples of other shadows than can be created using the same technique; a path like this
will produce a shadow like this
It's far from perfect but I think it does draw the sort of shadow you are looking for. Bear in mind that I have left a plain linear gradient in place from a total black to a clear color. Being so dark, this will not give you a super-realistic shadow unless you tweak the values a bit. You may want to play with the gradient by adding more locations with different alpha values to get whatever stepping you like. Some experimentation is probably required but the values are all there to play with.
As per your suggestion it's a drawRect:(CGRect)rect thing. Just create a custom view and only override it:
- (void)drawRect:(CGRect)rect {
// Get the context
CGContextRef context = UIGraphicsGetCurrentContext();
// Setup the gradient locations. We just want 0 and 1 as in the start and end of the gradient.
CGFloat locations[2] = { 0.0, 1.0 };
// Setup the two colors for the locations. A plain black and a plain black with alpha 0.0 ;-)
CGFloat colors[8] = { 0.0f, 0.0f, 0.0f, 1.0f, // Start color
0.0f, 0.0f, 0.0f, 0.0f }; // End color
// Build the gradient
CGGradientRef gradient = CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceRGB(),
colors,
locations,
2);
// Load a transformation matrix that will squash the gradient in the current context
CGContextScaleCTM(context,1.0f,0.1f);
// Draw the gradient
CGContextDrawRadialGradient(context, // The context
gradient, // The gradient
CGPointMake(self.bounds.size.width/2,0.0f), // Starting point
0.0f, // Starting redius
CGPointMake(self.bounds.size.width/2,0.0f), // Ending point
self.bounds.size.width/2, // Ending radius
kCGGradientDrawsBeforeStartLocation); // Options
// Release it an pray that everything was well written
CGGradientRelease(gradient);
}
This is how it looks like on my screen...
I simply placed an image just over the shadow but you can easily merge the shadow with an image if you subclass UIImageView and override it's drawRect method.
As you can see, what I did was to simply setup a circular gradient but I loaded a scaling matrix to squash it before drawing it to the context.
If you plan to do anything else in that method, remember that you have the matrix in place and everything you do will be deformed by it. You may want to save the the CTM with CGContextSaveGState() before loading the matrix and then restore the original state with CGContextRestoreGState()
Hope this was what you where looking for.
Cheers.
I could explain how to do this in code, or explain how to use a tool which generate this code for you. I choose the latter.
Using PaintCode (free demo available, 1 hour limit per session).
Draw an oval
Draw a Rectangle which intersects with the bottom of the oval.
CMD click both the rectangle and the oval, in the "Objects" list in the top left corner.
Press the Intersect button in the Toolbar.
Select the Bezier from the Objects list.
Set its Stroke to "No Stroke"
Click the Gradient button (located on the left, below the Selection Inspector)
Press the "+" button
Change the gradient color to light grey.
From the Selection inspector, change the Fill Style to "Gradient"
Select Gradient: Linear
adjust the gradient till you are satisfied.
- (void)viewDidLoad
{
UIImage *natureImage = [UIImage imageNamed:#"nature.jpg"];
CALayer *layer = [CALayer layer];
layer.bounds = CGRectMake(0, 0, 200, 200);
layer.position = CGPointMake(380, 200);
layer.contents = (id)natureImage.CGImage;
layer.shadowOffset = CGSizeMake(0,2);
layer.shadowOpacity = 0.70;
layer.shadowPath = (layer.shadowPath) ? nil : [self bezierPathWithCurvedShadowForRect:layer.bounds].CGPath;
[self.view.layer addSublayer:layer];
}
- (UIBezierPath*)bezierPathWithCurvedShadowForRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint topLeft = rect.origin;
CGPoint bottomLeft = CGPointMake(0.0, CGRectGetHeight(rect) + offset);
CGPoint bottomMiddle = CGPointMake(CGRectGetWidth(rect)/2, CGRectGetHeight(rect) - curve);
CGPoint bottomRight = CGPointMake(CGRectGetWidth(rect), CGRectGetHeight(rect) + offset);
CGPoint topRight = CGPointMake(CGRectGetWidth(rect), 0.0);
[path moveToPoint:topLeft];
[path addLineToPoint:bottomLeft];
[path addQuadCurveToPoint:bottomRight controlPoint:bottomMiddle];
[path addLineToPoint:topRight];
[path addLineToPoint:topLeft];
[path closePath];
return path;
}
Hope this will help you.

How does CGContextTransalateCTM work?

I am trying to draw a box/path in drawRect in one of my view's, the path rect is:
CGRect pathRect = CGRectMake(self.buttonSize_ + 25, commentYOffset, rect.size.width - 80, 40);
and before this I have:
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0.0, rect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
I wanted this path to be drawn 100 pixels from the top, so what I did is set that commentYOffset to 100, however this doesn't do it. What am I doing wrong so that when I draw this path it starts at 100 pixels from the top?
CGContextTranslateCTM(context,…) applies a translation transform to context. The translation is applied after any existing transform in the drawing context. Those last two lines in the code you posted effectively flip the vertical on your drawing context. Just leave them out and the path will be drawn where you want.
Edit: adit says he needs the transforms to keep some text drawing right side up, so: If you need to flip the vertical on your drawing context (as you're doing) then just remember that after applying these transformations zero-y is at the bottom of rect and the positive y-axis points towards the top of rect. So to draw your path 100 pixels from the top you should make
commentYOffset = CGRectGetMaxY(rect) - 40.0 - 100.0;
and that 40 is because your rect is 40 points tall. Actually it's better practice to make these named constants so you don't have to copy magic numbers all over the place going forward. Then your code will look something like this:
commentYOffset = CGRectGetMaxY(rect) - kPathRectHeight - kPathTopMargin;

How to animate a circle which is drawn in OpenGL?

I'm drawing several circles within my view by means of the drawRect function.
I'd like to have my circles pop up (scale to 1.2 -> scale to 1.0)
I've used coreanimation in the past but using OpenGL takes different functions.
Here's a snippit of my code which draws a circle in my view:
//calling draw function
CGContextRef contextRef = UIGraphicsGetCurrentContext();
//setting a specific fill color
CGContextSetRGBFillColor(contextRef, 0.0, 255.0, 0.0, 1.0);
//drawing the circle with a specific height, weight and x,y location
CGContextFillEllipseInRect(contextRef, CGRectMake(30 ,30, 20,20));
How can I animate this circle that it 'pops up'.
"Pops up" is not telling me that much.
if you want it to go from small to big modify width and height of rect.use a global int variable

CGImageCreateWithImageInRect causing distortion

I'm using CGImageCreateWithImageInRect to do a magnifying effect, and it works beautifully, except when I get close to the edges of my view. In that case, clipping causes the image to be distorted. Right now I grab a 72x72 chunk of the view, apply a round mask to it, and then draw the masked image, and a circle on top.
When the copied chunk is near the edge of the view, It winds up smaller than 72x72 because of clipping, and then when it's drawn in the magnifying glass it gets stretched out.
When the touch point is close to the left edge, for example, I would like to create an image where the left part is filled with a solid color, and the right half contains part of the view that's being magnified. Then apply the mask to that image and add the overlay on top.
Here's what I'm doing now. imageRef is the image being magnified, mask is a round mask, and overlay is a circle to mark the edges of the magnified region.
CGImageRef subImage = CGImageCreateWithImageInRect(imageRef, CGRectMake(touchPoint.x - 36, touchPoint.y - 36, 72, 72));
CGImageRef xMaskedImage = CGImageCreateWithMask(subImage, mask);
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform xform = CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, 0.0);
CGContextConcatCTM(context, xform);
CGRect area = CGRectMake(touchPoint.x - 84, -touchPoint.y, 170, 170);
CGRect area2 = CGRectMake(touchPoint.x - 80, -touchPoint.y + 4, 160, 160);
CGContextDrawImage(context, area2, xMaskedImage);
CGContextDrawImage(context, area, overlay);
I solved this by using CGBitmapContextCreate() to create a bitmap context. Then I drew the captured area into a smaller area of this context, and created an image from it with CGBitmapContextCreateImage(). That was the missing piece of the puzzle.

Clipping different parts of an image with path

I've recently asked a question about clipping an image via path at view's drawRect method.
iPhone clip image with path
Krasnyk's code is copied below.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
//or for e.g. CGPathAddRect(path, NULL, CGRectInset([self bounds], 10, 20));
CGPathAddEllipseInRect(path, NULL, [self bounds]);
CGContextAddPath(context, path);
CGContextClip(context);
CGPathRelease(path);
[[UIImage imageNamed:#"GC.png"] drawInRect:[self bounds]];
}
It works very well. However, when my image is larger than the view itself, how do I show different parts of the image?
I tried tweaking around with translation on the locations (show as bounds above) of ellipse and/or UIImage drawInRect but some complex effects (Unwanted clipping, weird elipse size) I can't explain happens.
EDIT: Maybe I can answer my own question. I can try drawAtPoint instead of drawInRect? Or drawInRect and set the origin to a different location, but expand the size of the rectangle at the same time?
Does that incur performance penalty when I am drawing a larger rectangle than is showing through the view?
Sounds like you've about figured it out for yourself. drawAtPoint is what you should use. drawInRect will scale the image to fit the destination rect, which is computationally more expensive. Assuming your image is larger than your view, you'll be passing negative x and y values to drawAtPoint in order to clip interior portions of the image.
The example below should display the center portion your image, within your view:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, NULL, [self bounds]);
CGContextAddPath(context, path);
CGContextClip(context);
CGPathRelease(path);
UIImage *bigImage = [UIImage imageNamed:#"GC.png"];
CGPoint topLeftOffset = CGPointMake((self.bounds.size.width - bigImage.size.width) / 2,(self.bounds.size.height - bigImage.size.height) / 2);
[bigImage drawAtPoint: topLeftOffset];
}