CAShapeLayer filling with another CAShapeLayer as a mask - ios

My question is about filling animation one CAShapeLayer with another CAShapeLayer. So, I wrote some code that almost achieves my goal.
I created a based layer and fill it with another. Code below:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_shapeLayer = [CAShapeLayer layer];
[self drawBackgroundLayer];
}
// my main/backgound layer
- (void)drawBackgroundLayer {
_shapeLayer.frame = CGRectMake(0, 0, 250, 250);
_shapeLayer.lineWidth = 3;
_shapeLayer.strokeColor = [UIColor blackColor].CGColor;
_shapeLayer.fillColor = UIColor.whiteColor.CGColor;
_shapeLayer.path = [UIBezierPath bezierPathWithRect:_shapeLayer.bounds].CGPath;
[self.view.layer addSublayer:_shapeLayer];
[self createCircleLayer];
}
Create a circled mask layer for filling:
- (void)createCircleLayer {
_shapeLayer.fillColor = UIColor.greenColor.CGColor;
CAShapeLayer *layer = [CAShapeLayer layer];
CGFloat radius = _shapeLayer.bounds.size.width*sqrt(2)/2;
layer.bounds = CGRectMake(0, 0, 2*radius, 2*radius);
layer.position = CGPointMake(_shapeLayer.bounds.size.width/2, _shapeLayer.bounds.size.height/2);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:layer.bounds cornerRadius:radius];
layer.path = path.CGPath;
layer.transform = CATransform3DMakeScale(0.1, 0.1, 1);
_shapeLayer.mask = layer;
}
// our result is next]
So, let's animate it. I just transform the layer to fill the whole "_shapeLayer".
- (void)animateMaskLayer:(CAShapeLayer *)maskLayer {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1, 1, 1.0)];
animation.duration = 1;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[newLayer addAnimation:animation forKey:#"transform"];
}
Now, I want to do the same with this green layer (fill it with red color).
But when I use the mask I have red circle and the white background what is obviously.
My goal here is to have red circle and green background. Like normal filling with another color.
By using inverse mask won't help neither.
So, I just have no ideas how to do it.
If you suggest using:
[_shapeLayer addSublayer:layer];
it won't work as there will be not only square but different paths and clipToBounds is not an option.
Thank you for any help!

So, the solution is simple. I just use two layers and one mask layer.
First layer: Red layer I want to fill with green layer;
- (void)drawBackgroundLayer {
_shapeLayer.frame = CGRectMake(20, 20, 250, 250);
_shapeLayer.lineWidth = 3;
_shapeLayer.strokeColor = [UIColor blackColor].CGColor;
_shapeLayer.lineWidth = 3;
_shapeLayer.fillColor = UIColor.redColor.CGColor;
_shapeLayer.path = [UIBezierPath bezierPathWithRect:_shapeLayer.bounds].CGPath;
[self.view.layer addSublayer:_shapeLayer];
[self drawUpperLayer];
}
Second Layer: Green Layer I use for filling.
- (void)drawUpperLayer {
_upperLayer = [CAShapeLayer layer];
_upperLayer.frame = _shapeLayer.frame;
_upperLayer.lineWidth = 3;
_upperLayer.strokeColor = [UIColor blackColor].CGColor;
_upperLayer.fillColor = UIColor.greenColor.CGColor;
_upperLayer.path = [UIBezierPath bezierPathWithRect:_shapeLayer.bounds].CGPath;
[_shapeLayer.superlayer addSublayer:_upperLayer];
}
Mask Layer:
- (void)createMaskLayer {
CAShapeLayer *layer = [CAShapeLayer layer];
CGFloat radius = _upperLayer.bounds.size.width*sqrt(2)/2;
layer.bounds = CGRectMake(0, 0, 2*radius, 2*radius);
layer.fillColor = UIColor.blackColor.CGColor;
layer.position = CGPointMake(_upperLayer.bounds.size.width/2, _upperLayer.bounds.size.height/2);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:layer.bounds cornerRadius:radius];
layer.path = path.CGPath;
layer.transform = CATransform3DMakeScale(0.1, 0.1, 1);
_upperLayer.mask = layer;
}
Animate mask layer:
- (void)animateMaskLayer:(CAShapeLayer *)maskLayer {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1, 1, 1.0)];
animation.duration = 2;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[maskLayer addAnimation:animation forKey:#"transform"];
}
So, resulted animation:

Related

Animate progress with filling the color of the border width of a UIImageView layer

I have am UIImageView and I have made it to be circle with a width layer like in this image :
User can update the image and upload new one, I have a progress call back while image is uploaded. What I want is to animate the border with color while the image is uploaded, for example when user click upload the border start from top with green color and fill the width according to the progress.
I tried with this code:
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointZero radius:27 startAngle:-M_PI_2 endAngle:2 * M_PI - M_PI_2 clockwise:YES].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor greenColor].CGColor;
circle.lineWidth = 4;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 10;
animation.removedOnCompletion = NO;
animation.fromValue = #(0);
animation.toValue = #(1);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circle addAnimation:animation forKey:#"drawCircleAnimation"];
[imageView.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
[imageView.layer addSublayer:circle];
But It shows the circle in wrong position and this aproch does not take a progress, it's static is there any way to fill the border width of the layer of UIImageView according to progress ?
UIImageView is meant to show the images. I would not suggest changing it's behaviour. Even though you subclass the UIImageView and try to draw something in it, the drawRect of it will not get called. Let the imageView be imageView and use UIView for the rest
I suggest you to subclass the UIView and then draw a bezier path in it and animate the bezier path curve, You can later add the imageview to the uiview .
UIView subclass:
- (void)drawRect:(CGRect)rect {
[self drawImageHolderViewFrame:rect startAngle:_startAngle];
}
- (void)drawImageHolderViewFrame: (CGRect)frame startAngle: (CGFloat)startAngle
{
CGRect ovalRect = CGRectMake(CGRectGetMinX(frame) , CGRectGetMinY(frame) , frame.size.width-10, frame.size.height-10);
UIBezierPath* ovalPath = [UIBezierPath bezierPath];
[ovalPath addArcWithCenter: CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)) radius: CGRectGetWidth(ovalRect) / 2 startAngle: -91 * M_PI/180 endAngle: -startAngle * M_PI/180 clockwise: YES];
[UIColor.redColor setStroke];
ovalPath.lineWidth = 2;
[ovalPath stroke];
}
viewController class:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
_imageHolderView=[[MyView alloc]initWithFrame:CGRectMake(20, 20, 100, 100)];
_imageHolderView.startAngle=90;
_imageHolderView.backgroundColor=[UIColor clearColor];
[self.view addSubview: _imageHolderView];
[self startTImer];
}
-(void)startTImer{
NSTimer *timer=[NSTimer scheduledTimerWithTimeInterval:0.02 target:self selector:#selector(animateBorder) userInfo:nil repeats:YES];
}
-(void)animateBorder{
if (_startAngle<=-270) {
_startAngle=90;
}
_startAngle--;
_imageHolderView.startAngle=_startAngle;
[_imageHolderView setNeedsDisplay];
}
This would give you:
Now you can add the imageview as a subview to the view you just created.
Please try the code below:
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(29, 29) radius:27 startAngle:-M_PI_2 endAngle:2 * M_PI - M_PI_2 clockwise:YES].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor greenColor].CGColor;
circle.lineWidth = 4;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 10;
animation.removedOnCompletion = NO;
animation.fromValue = #(0);
animation.toValue = #(1);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[circle addAnimation:animation forKey:#"drawCircleAnimation"];
[imageCircle.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
[imageCircle.layer addSublayer:circle];

UIView circle mask animation

I have an abstract strange shaped UIView.
And I need to display a smooth appearance animation.
I assume I should apply that animation to the mask above that view.
The mask is going to be circle shaped.
So user will see the 1'2'3'4' sequence.
How can I implement that?
I suppose I should apply some affine transformation
to the mask view. In that case, what should be the original shape of the mask?
A vertical line?
A simple example,suppose I have a 100*100 imageview,
Note:to make it simple,I use hard code numbers
CAShapeLayer * maskLayer = [CAShapeLayer layer];
maskLayer.bounds = CGRectMake(0, 0, 100, 100);
maskLayer.position = CGPointMake(50, 50);
UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(50.0, 50.0) radius:50 startAngle:0 endAngle:M_PI *2 clockwise:true];
maskLayer.path = path.CGPath;
maskLayer.fillColor = [UIColor clearColor].CGColor;
maskLayer.strokeColor = [UIColor blackColor].CGColor;
maskLayer.strokeEnd = 0.0;
maskLayer.lineWidth = 100;
self.imageview.layer.mask = maskLayer;
CABasicAnimation * animation = [CABasicAnimation animation];
animation.keyPath = #"strokeEnd";
animation.toValue = #(1.0);
animation.duration = 2.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
[maskLayer addAnimation:animation forKey:#"keyFrame"];

Animate transformation from a line to a curve

I encountered a problem when drawing an animated bounce curve.
The code below allow the desired effect animating the line, however when curving i do not want to see the straight line. I had i done wrong with my code ? any comment or guidances are greatly appreciated.
Cheers
- (void)loadView
{
//up Path
upPath = CGPathCreateMutable();
CGPathMoveToPoint(upPath, nil,startingDrawPointX,startingDrawPointY);
CGPathAddQuadCurveToPoint(upPath, nil, controlPoint.x, 100, endingDrawPointX, endingDrawPointY);
CGPathCloseSubpath(upPath);
//down Path
downPath = CGPathCreateMutable();
CGPathMoveToPoint(downPath, nil,startingDrawPointX,startingDrawPointY);
CGPathAddQuadCurveToPoint(downPath, nil, controlPoint.x, 70, endingDrawPointX, endingDrawPointY);
CGPathCloseSubpath(downPath);
//mid Path
midPath = CGPathCreateMutable();
CGPathMoveToPoint(midPath, nil,startingDrawPointX,startingDrawPointY);
CGPathAddQuadCurveToPoint(midPath, nil, controlPoint.x, controlPoint.y, endingDrawPointX, endingDrawPointY);
CGPathCloseSubpath(midPath);
//Create Shape
shapeLayer = [CAShapeLayer layer];
// shapeLayer.path = midPath;
shapeLayer.fillColor = nil;
shapeLayer.strokeColor = kDBrownTextColor.CGColor;
shapeLayer.lineWidth = 2.0;
// shapeLayer.fillRule = kCAFillRuleNonZero;
[self.layer addSublayer:shapeLayer];
[self performSelector:#selector(startAnimation) withObject:nil afterDelay:1.5];
}
-(void)startAnimation
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 1;
animation.repeatCount = maxCount;
animation.autoreverses = YES;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (__bridge id)downPath;
animation.fromValue =(__bridge id)midPath;
animation.toValue = (__bridge id)upPath;
[shapeLayer addAnimation:animation forKey:#"animatePath"];
}
Comment out the lines with CGPathCloseSubpath - they are creating the straight lines you see

How to put 2 layers to follow same bezierpath without one hiding the other

I have created 2 CALayers of same size and am passing these to the method below. However, the two layers runs together. How can I seperate these so that both are visible?
- (void) myAnimation : (CALayer *) sublayer {
UIBezierPath* aPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(30, 100, 270, 270)];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = aPath.CGPath;
anim.rotationMode = kCAAnimationRotateAuto;
anim.repeatCount = HUGE_VALF;
anim.duration =35.0;
[sublayer addAnimation:anim forKey:#"race"];
}
Your path begins and ends at the same point. Assuming both beginning times are the same and duration is the same, your layers will precisely overlap. You can either shift the beginning time or rotate your UIBezierPath* aPath Here's an example of rotating your UIBezierPath* aPath and altering the duration. It should give you an idea how you could alter duration, begin time, rotation, etc.
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor blackColor]];
CALayer * layer1 = [CALayer layer];
CALayer * layer2 = [CALayer layer];
[layer1 setFrame:CGRectMake(0, 0, 5, 50)];
[layer2 setFrame:CGRectMake(0, 0, 5, 100)];
layer1.backgroundColor = [[UIColor colorWithRed:.1 green:.5 blue:1 alpha:.35] CGColor];
layer2.backgroundColor = [[UIColor colorWithRed:.9 green:.2 blue:.5 alpha:.35] CGColor];
[self.view.layer addSublayer:layer1];
[self.view.layer addSublayer:layer2];
[self myAnimation:layer1 andRotation:0 andDuration:35.0];
[self myAnimation:layer2 andRotation:10 andDuration:10.0];
}
- (void) myAnimation:(CALayer *)sublayer andRotation:(CGFloat)rot andDuration:(CFTimeInterval)dur {
CGRect rect = CGRectMake(30, 100, 270, 270);
UIBezierPath* aPath = [UIBezierPath bezierPathWithOvalInRect:rect];
// Creating a center point around which we will transform the path
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, center.x, center.y);
transform = CGAffineTransformRotate(transform, rot);
// Recenter the transform
transform = CGAffineTransformTranslate(transform, -center.x, -center.y);
// This is the new path we will use.
CGPathRef rotated = CGPathCreateCopyByTransformingPath(aPath.CGPath, &transform);
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = rotated;
anim.rotationMode = kCAAnimationRotateAuto;
anim.repeatCount = HUGE_VALF;
anim.duration =dur;
[sublayer addAnimation:anim forKey:#"race"];
}

How do I correctly position an animated CAShapeLayer

I'm animating the end stroke of a CAShapeLayer, but the position is incorrect and I can't figure out what I'm doing wrong. The frame is correct, but when I try and adjust the position I can't get it to "fill" the frame's width of the view. I've tried to adjust the _maskLine's position, but my approaches have failed thus far.
The frame of the view is: 10, 10, 50, 100
See image (the red shape layer should be "shifted" over 10 pts.
// layer setup in initWithFrame
_maskLine = [CAShapeLayer layer];
_maskLine.lineCap = kCALineCapButt;
_maskLine.fillColor = [[UIColor whiteColor] CGColor];
_maskLine.lineWidth = frame.size.width;
_maskLine.strokeEnd = 0.0;
_maskLine.frame = self.bounds;
[self.layer addSublayer:_maskLine];
self.clipsToBounds = YES;
self.layer.cornerRadius = 2.0f;
// animation method logic
UIBezierPath *progressline = [UIBezierPath bezierPath];
NSUInteger randomInt = arc4random() % (NSUInteger)self.maxMeterHeight;
self.meterHeight = (randomInt / self.maxMeterHeight);
[progressline moveToPoint:CGPointMake(CGRectGetMidX(self.frame), CGRectGetHeight(self.frame))];
[progressline addLineToPoint:CGPointMake(CGRectGetMidX(self.frame), (1 - self.meterHeight) * CGRectGetHeight(self.frame))];
[progressline setLineWidth:1.0];
[progressline setLineCapStyle:kCGLineCapSquare];
self.maskLine.path = progressline.CGPath;
self.maskLine.strokeColor = [UIColor redColor].CGColor;
self.maskLine.frame = self.bounds;
NSLog(#"mask frame: %#", NSStringFromCGRect(self.maskLine.frame));
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 0.35;
pathAnimation.autoreverses = YES;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pathAnimation.fromValue = #0.0f;
pathAnimation.toValue = #1.0f;
pathAnimation.cumulative = YES;
[self.maskLine addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
[CATransaction commit];
self.maskLine.strokeEnd = 1.0;
The "fix" was changing these lines
[progressline moveToPoint:CGPointMake(CGRectGetMidX(self.frame), CGRectGetHeight(self.frame))];
[progressline addLineToPoint:CGPointMake(CGRectGetMidX(self.frame), (1 - self.meterHeight) * CGRectGetHeight(self.frame))];
to:
[progressline moveToPoint:CGPointMake(self.frame.size.width / 2, CGRectGetHeight(self.frame))];
[progressline addLineToPoint:CGPointMake(self.frame.size.width / 2, (1 - self.meterHeight) * CGRectGetHeight(self.frame))];

Resources