How do I correctly position an animated CAShapeLayer - ios

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))];

Related

CAShapeLayer filling with another CAShapeLayer as a mask

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:

How to Draw and animate the wifi signal pulse

i want to draw an wifi signal pulse and want to animate with that in launch screen. i tried with this code. the signal is showing in the down words but i need to show in up words. Starting from a point ti increase [like our phone wifi signal]
- (void)viewDidLoad
{
[super viewDidLoad];
[self addWavesToView];
[self addAnimationsToLayers];
}
- (void)addWavesToView
{
CGRect rect = CGRectMake(0.0f, 0.0f, 300.0f, 300.0f);
UIBezierPath *circlePath = [UIBezierPath
bezierPathWithOvalInRect:rect];
for (NSInteger i = 0; i < 10; ++i) {
CAShapeLayer *waveLayer = [CAShapeLayer layer];
waveLayer.bounds = rect;
waveLayer.position = CGPointMake(100, 100);
waveLayer.strokeColor = [[UIColor lightGrayColor] CGColor];
waveLayer.fillColor = [[UIColor clearColor] CGColor];
waveLayer.lineWidth = 5.0f;
waveLayer.path = circlePath.CGPath;
waveLayer.transform = CATransform3DMakeTranslation(0.0f, i*25, 0.0f);
waveLayer.strokeStart = 0.25- ((i+1) * 0.01f);
waveLayer.strokeEnd = 0.25+((i+1) * 0.01f);
[self.view.layer addSublayer:waveLayer];
}
}
- (void)addAnimationsToLayers
{
NSInteger timeOffset = 10;
for (CALayer *layer in self.view.layer.sublayers) {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeColor"];
animation.duration = 10.0f;
// Stagger the animations so they don't begin until the
// desired time in each
animation.timeOffset = timeOffset--;
animation.toValue = (id)[[UIColor lightGrayColor] CGColor];
animation.fromValue = (id)[[UIColor darkGrayColor] CGColor];
// Repeat forever
animation.repeatCount = HUGE_VALF;
// Run it at 10x. You can adjust this to taste.
animation.speed = 10;
// Add to the layer to start the animation
[layer addAnimation:animation forKey:#"strokeColor"];
}
"show in up words", it'll take more time so we can rotate base view to 180 degree by this code inside of viewDidLoad() Function
CGFloat degreesOfRotation = 180.0;
self.view.transform = CGAffineTransformRotate(self.view.transform,
degreesOfRotation * M_PI/180.0);

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];

iOS - Pie progress bar with rounded corners

I need to implement a progress bar according to this design :
As you can see, there is a corner radius to the bar itself.
This is how it looks now with my current code :
So, how to do that?
This is my current code:
- (void)animateProgressBarToPercent:(float)percent
{
if (percent > 1.0f) return;
int radius = 42.7f;
int strokeWidth = 7.f;
CGColorRef color = [UIColor someColor].CGColor;
int timeInSeconds = percent * 5;
CGFloat startAngle = 0;
CGFloat endAngle = percent;
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius].CGPath;
circle.position = CGPointMake(self.center.x - radius, self.center.y + strokeWidth);
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = color;
circle.lineWidth = strokeWidth;
circle.strokeEnd = endAngle;
[self.layer addSublayer:circle];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = timeInSeconds;
drawAnimation.repeatCount = 1.0;
drawAnimation.removedOnCompletion = NO;
drawAnimation.fromValue = [NSNumber numberWithFloat:startAngle];
drawAnimation.toValue = [NSNumber numberWithFloat:endAngle];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
circle.lineCap = kCALineCapRound
Just set this property of your CAShapeLayer

Re-draw UIBezierPath based on random CGFloat value

I am developing a custom circular progress bar using UIBezierPath. I want to change the progress based on randomly generated CGFloat values.
My progress bar looks like this:
I drew the progress bar using the following code:
// Draw the arc with bezier path
int radius = 100;
CAShapeLayer *arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 50) radius:radius startAngle:M_PI endAngle:M_PI/150 clockwise:YES].CGPath;
arc.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
arc.cornerRadius = 100.0;
arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor purpleColor].CGColor;
arc.lineWidth = 6;
[self.view.layer addSublayer:arc];
// Animation of the progress bar
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = YES; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:10.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
// Gradient of progress bar
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.view.frame;
gradientLayer.colors = #[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor purpleColor].CGColor ];
gradientLayer.startPoint = CGPointMake(0,0.1);
gradientLayer.endPoint = CGPointMake(0.5,0.2);
[self.view.layer addSublayer:gradientLayer];
gradientLayer.mask = arc;
refreshButton = [[UIButton alloc] initWithFrame:CGRectMake(50, 370, 50, 50)];
[refreshButton addTarget:self action:#selector(refresh:) forControlEvents:UIControlEventTouchUpInside];
[refreshButton setBackgroundImage:[UIImage imageNamed:#"refresh.png"] forState:UIControlStateNormal];
[self.view addSubview:refreshButton];
The UIButton with the green arrow generates randomly CGFloat values. So my question is how to reset the animation so that it stops at the corresponding position if it is between 0.0 and 1.0? I tried to insert the code for the animation inside the method of the button, but as a result the new arc was drawn on top of the existing one.
-(void)refresh:(id)sender{
NSLog(#"Refresh action:");
CGFloat randomValue = ( arc4random() % 256 / 256.0 );
NSLog(#"%f", randomValue);
valueLabel.text = #"";
valueLabel = [[UILabel alloc] initWithFrame:CGRectMake(150, 370, 100, 50)];
valueLabel.text = [NSString stringWithFormat: #"%.6f", randomValue];
[self.view addSubview:valueLabel];
// Draw the arc with bezier path
int radius = 100;
CAShapeLayer *arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 50) radius:radius startAngle:M_PI endAngle:M_PI/150 clockwise:YES].CGPath;
arc.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
arc.cornerRadius = 100.0;
arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor purpleColor].CGColor;
arc.lineWidth = 6;
[self.view.layer addSublayer:arc];
// Animation of the progress bar
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = YES; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:randomValue];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
I have no clue how to re-draw the arc, so any help or idea would be greatly appreciated!
Thanks a lot.
Granit
Try re-using the sublayer or replacing it. Make arc a class variable. Then cycle through the sublayers, remove the old arc then remake it with your new random float percentage. Once arc is a class variable then you can either replace it and animate from zero or you can animate from it's current percent to a new percent.

Resources