I am using the TapkuLibrary's otherwise excellent TKCalendarDayEventView and trying to selectively round one of the corners in the view as StuDev demonstrates here. Unfortunately, applying StuDev's code snippet results in the EventView disappearing entirely from its containing TKCalendarDayTimelineView. I am adding this code snippet underneath the current code in the
+ (id)eventViewWithFrame:(CGRect)frame id:(NSNumber *)id startDate:(NSDate *)startDate endDate:(NSDate *)endDate title:(NSString *)title location:(NSString *)location;
method. I have commented out code that otherwise sets the border width, color, or radius in the code. I have made sure that TKCalendarDayEventView doesn't have any superlayers, since the
apple docs warn against adding masks to layers with superlayers:
When setting the mask to a new layer, the new layer’s superlayer must first be set to nil, otherwise the behavior is undefined.
I have also tried playing around with the backgroundColor and fillColor properties of the maskLayer. I don't see anything in the TKCalendarDayEventView that might stop this mask from being correctly applied. What could I be doing wrong?
If you put a breakpoint in your eventViewWithFrame:id:startDate:endDate:title:location: method, you will see that when you create your event view you are setting the frame to CGRectZero. The code snippet that then sets the rounded corner mask is using CGRectZero as the mask layer's frame.
Probably the simplest way to deal with this would be to override TKCalendarDayEventView's setFrame: method like so:
- (void)setFrame:(CGRect)newFrame
{
if (!CGRectEqualToRect([super frame], newFrame)) {
[super setFrame:newFrame];
// Change the view's mask layer to fit the new frame.
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds
byRoundingCorners:UIRectCornerTopLeft
cornerRadii:CGSizeMake(15.0, 15.0)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
}
}
This way, every time you change the frame of the view the mask automatically adjusts.
Related
In simple words only part of my icon must change color at some event.
I drawing an icon with curves inside an UIView. The drawing boilds
down to combining several paths together like this (swift code):
let combinedPath = CGPathCreateMutableCopy(bezierPath.CGPath)
CGPathAddPath(combinedPath, nil, bezier2Path.CGPath);
CGPathAddPath(combinedPath, nil, bezier3Path.CGPath);
Then I'm adding the combined path to the UIView like this (obj-c, sorry for
the mix):
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.frame = self.bounds;
shapeLayer.path = combinedPath;
self.layer.mask = shapeLayer;
And now at some point in time I want to change the fill color of
only one bezierPath with animation. How can I do this?
You can clean the current mask, and redraw any of the bezierpath with stroke/fill color and combine them together, and set it again. And for drawing animation, you can refer to How to Animate CoreGraphics Drawing of Shape Using CAKeyframeAnimation
I have a UIView, with view.layer.mask set to an instance of CAShapeLayer. The shape layer contains a path, and now I want to add a hole to this shape by adding a second shape with even/odd rule, and fade-in the appearance of this hole.
The problem is that adding to path doesn't seem to be animatable:
[UIView animateWithDuration:2 animations:^() {
CGPathAddPath(parentPath, nil, holePath);
[v.layer.mask didChangeValueForKey:#"path"];
}];
How would I animate this?
After some fiddling, found a workaround:
Create a layer with two sublayers with two desired shapes, and use it as a mask
Animate opacity of the first sublayer (without a hole) from 1 to 0.
This works because child CAShapeLayer instances appear to be used as a union. When you hide the first sublayer without a hole, only the hole will be uncovered, the shared area will not change.
CGMutablePathRef p = CGPathCreateMutable();
// First path
CGPathAddPath(p, nil, outerPath);
CAShapeLayer *mask1 = [CAShapeLayer layer];
mask1.path = p;
// 2nd path with a hole
CGPathAddPath(p, nil, innerPath);
CAShapeLayer *mask2 = [CAShapeLayer layer];
mask2.path = p;
mask2.fillRule = kCAFillRuleEvenOdd;
CGPathRelease(p);
// Create actual mask that hosts two submasks
CALayer *mask = [CALayer layer];
[mask addSublayer:mask1];
[mask addSublayer:mask2];
myView.layer.mask = mask;
mask.frame = v.layer.bounds;
mask1.frame = mask.bounds;
mask2.frame = mask.bounds;
// ...
// Hide mask #1
CABasicAnimation *a = [CABasicAnimation animationWithKeyPath:#"opacity"];
a.fromValue = #1.0;
a.toValue = #0.0;
a.duration = 1.0;
a.fillMode = kCAFillModeForwards; // Don't reset back to original state
a.removedOnCompletion = NO;
[mask1 addAnimation:a forKey:#"hideMask1"];
You can't use UIView animation to animate CALayers.
Most layer property changes do animation by default (implicit animation). As I recall, shape layer path changes are an exception to that.
You'll need to create a CAAnimation object where the property you are animating is the path on your mask layer.
However, that probably won't give the effect you want. The reason is that when you change a path on a shape layer, Core Animation tries to animate the change in shape of the path. Furthermore, path changes only work properly when the starting and ending paths have the same number and type of control points.
I'm not sure how you'd achieve a cross-fade between 2 different masks without a lot of work.
Off the top of my head, the only way I can think of to do this would be to create a snapshot of the new view appearance with the changed mask (probably using Core Image filters) and then do a cross-fade of a layer that displays that snapshot. Once the crossfade is complete, you would install the new path in your mask layer without animation and then remove the snapshot bitmap, revealing the real view underneath.
There might be a simpler way to achieve what you're after but I don't know what that would be. Maybe one of the CA experts that contributes to SO could chime in here.
Example project: http://cl.ly/1l1x1A0J3o2X
I have a child view controller that has a UITableView in it, and this view controller sits on top of another view controller. I want to give the bottom of it rounded corners (but only the bottom).
In my UITableView subclass I have this code to round the bottom corners.
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
UIBezierPath *maskPath;
maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:(UIRectCornerBottomLeft | UIRectCornerBottomRight) cornerRadii:CGSizeMake(5.0, 5.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
self.backgroundColor = [UIColor colorWithRed:0.169 green:0.169 blue:0.169 alpha:1];
}
return self;
}
Which does indeed round them, but as soon as I scroll the table view it moves it up further and decreases in height (seemingly at least).
However, if I simply use self.layer.cornerRadius = 5.0 it completely operates fine.
Does anyone know why this behaviour might be the case?
UIScrollView, and thus the subclass UITableView, actually renders its content by changing the bounds of what it's rendering. My guess is that adding a mask to the UITableView's layer is actually adding the mask to the content within the view, not the container like you might imagine.
Why 'self.layer.cornerRadius = 5.0' works, and your approach doesn't, I am not sure.
An approach that would work better was already given for a similar request, so I'll direct you over to:
Adding Rounded Corners to only top of UITableView?
Basically, the recommendation was to put the UITableView into a container and add the mask to the container. I believe that would work just fine.
This might not be an exact answer. But I am gonna give you an workaround.
Look at this question and answer.
How to get an UITableView working with CAShapeLayer mask?
https://stackoverflow.com/a/11538923/1083859
This will give you a clue what I tried.
As in that question explained, adding tableView mask will not going to work for you, because it will move with your tableView as you explained.
So I tried adding a another superview for the tableView in your DropDownViewController and add a outlet for it.
Then I changed your code inside the viewDidLoad in DropDownViewController to add the mask for current superview of tableView like below.
UIBezierPath *maskPath;
maskPath = [UIBezierPath bezierPathWithRoundedRect:self.tableSuperView.bounds byRoundingCorners:(UIRectCornerBottomLeft | UIRectCornerBottomRight) cornerRadii:CGSizeMake(5.0, 5.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.view.bounds;
maskLayer.path = maskPath.CGPath;
self.tableSuperView.layer.mask = maskLayer;
and that's it. It works. And I found an unwanted behavior when scroll the tableView to the end. It happens because of tableView bounce animation. If you remove that animation, then you are good to go.
Glad to know your opinion and result.
I am wondering if it is possible to clip a view to a Bezier Path. What I mean is that I want to be able to see the view only in the region within the closed Bezier Path. The reason for this is that I have the outline of an irregular shape, and I want to fill in the shape gradually with a solid color from top to bottom. If I could make it so that a certain view is only visible within the path then I could simply create a UIView of the color I want and then change the y coordinate of its frame as I please, effectively filling in the shape. If anyone has any better ideas for how to implement this that would be greatly appreciated. For the record the filling of the shape will match the y value of the users finger, so it can't be a continuous animation. Thanks.
Update (a very long time later):
I tried your answer, Rob, and it works great except for one thing. My intention was to move the view being masked while the mask remains in the same place on screen. This is so that I can give the impression of the mask being "filled up" by the view. The problem is that with the code I have written based on your answer, when I move the view the mask moves with it. I understand that that is to be expected because all I did was add it as the mask of the view so it stands to reason that it will move if the thing it's tied to moves. I tried adding the mask as a sublayer of the superview so that it stays put, but that had very weird results. Here is my code:
self.test = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
self.test.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.test];
UIBezierPath *myClippingPath = [UIBezierPath bezierPath];
[myClippingPath moveToPoint:CGPointMake(100, 100)];
[myClippingPath addCurveToPoint:CGPointMake(200, 200) controlPoint1:CGPointMake(self.screenWidth, 0) controlPoint2:CGPointMake(self.screenWidth, 50)];
[myClippingPath closePath];
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
self.test.layer.mask = mask;
CGRect firstFrame = self.test.frame;
firstFrame.origin.x += 100;
[UIView animateWithDuration:3 animations:^{
self.test.frame = firstFrame;
}];
Thanks for the help already.
You can do this easily by setting your view's layer mask to a CAShapeLayer.
UIBezierPath *myClippingPath = ...
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
myView.layer.mask = mask;
You will need to add the QuartzCore framework to your target if you haven't already.
In Swift ...
let yourCarefullyDrawnPath = UIBezierPath( .. blah blah
let maskForYourPath = CAShapeLayer()
maskForYourPath.path = carefullyRoundedBox.CGPath
layer.mask = maskForYourPath
Just an example of Rob's solution, there's a UIWebView sitting as a subview of a UIView called smoothView. smoothView uses bezierPathWithRoundedRect to make a rounded gray background (notice on right). That works fine.
But if smoothView has only normal clip-subviews, you get this:
If you do what Rob says, you get the rounded corners in smoothView and all subviews ...
Great stuff.
The question
How do you create a separate context for your layer and incorporate that context into super layer?
Why I want to do this
I want abstract the drawing of a view layers into separate objects/files. I want to construct a view out of layers, then position then on top of one another and have other possibilities as that.
The problem is that I'm not aware of you you're supposed to draw a part of your view into a layer without drawing straight into the context of the main views sublayer.
Here's an example, I have subclassed CALayer with HFFoundation:
#implementation HFFoundation
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
UIBezierPath *foundation = [UIBezierPath bezierPath];
// some drawing goes on here in the form of messages to [foundation]
}
#end
Now when I instantiate my custom layer inside a views (void)drawRect:(CGRect)rect I this way:
HFFoundation *foundation = [HFFoundation new];
foundation.frame = CGRectMake(40, viewHeight - 100, 300, 100);
foundation.backgroundColor = [[UIColor colorWithRed:1 green:.4 blue:1 alpha:0.2] CGColor];
[self.window.rootViewController.view.layer addSublayer:foundation];
I get this result (no drawings appear, just the bg color inside the layer frame):
If I [foundation drawLayer:foundation inContext:context]; afterwards, the drawing appears, but it appears inside the top layers context, not in foundation layer. Since foundation layer is also lower in the hierarchy, it hides the drawing (unless I reduce it's alpha, which I've dont in the picture):
How do you draw into the layer itself, I.e. foundation in this case?
To insert a sublayer and set its index you want this piece of code:
[view.layer insertSublayer:foundation atIndex:0];