I would like to change the curve's Y position which acts like a UISlider with animation.
I have a method drawCurve that will draw a quadCurve, but I would like to animate it. I'm not too sure how I should link it up with CABasicAnimation.
//draw line path
-(void)drawCurve
{
//ensure the drag up or down just in the middle
if (controlPoint.x <= halfWidth || controlPoint.x >= halfWidth) {controlPoint.x = halfWidth;}
if (controlPoint.y <= 0 ) {controlPoint.y = 0;}
if (controlPoint.y >= self.frame.size.height) {controlPoint.y = self.frame.size.height;}
CGContextRef ctx = UIGraphicsGetCurrentContext();
curvePath = CGPathCreateMutable();
CGPathMoveToPoint(curvePath, NULL, startingDrawPointX, startingDrawPointY);
CGPathAddQuadCurveToPoint(curvePath, NULL, controlPoint.x, controlPoint.y, endingDrawPointX, endingDrawPointY);
CGContextAddPath(ctx, curvePath);
CGContextSetStrokeColorWithColor(ctx,kDBrownTextColor.CGColor);
CGContextSetLineWidth(ctx, 2.0);//thickness
CGContextStrokePath(ctx);
CGPathRelease(curvePath);
}
-(void)startAnimation
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 1;
animation.repeatCount = 5;
animation.autoreverses = NO;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (__bridge id)currentCurvePath;
animation.toValue = (__bridge id)ToPath;
[self.layer.superlayer addAnimation:animation forKey:#"animatePath"];
}
Here is an example of what I think your trying to accomplish, using a CAShapeLayer.
+(Class) layerClass {
return [CAShapeLayer class];
}
//draw line path
-(void)drawCurve
{
//ensure the drag up or down just in the middle
if (controlPoint.x <= halfWidth || controlPoint.x >= halfWidth) {controlPoint.x = halfWidth;}
if (controlPoint.y <= 0 ) {controlPoint.y = 0;}
if (controlPoint.y >= self.frame.size.height) {controlPoint.y = self.frame.size.height;}
//create the path
curvePath = CGPathCreateMutable();
CGPathMoveToPoint(curvePath, NULL, startingDrawPointX, startingDrawPointY);
CGPathAddQuadCurveToPoint(curvePath, NULL, controlPoint.x, controlPoint.y, endingDrawPointX, endingDrawPointY);
//add the path to the background layer
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
layer.path = curvePath;
layer.strokeColor = [UIColor brownColor].CGColor;
layer.lineWidth = 2.0;
//release the path
CGPathRelease(curvePath);
}
-(void)startAnimation {
[CATransaction begin];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 1;
animation.repeatCount = 5;
animation.autoreverses = NO;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (__bridge id)curvePath;
animation.toValue = (__bridge id)ToPath;
[self.layer addAnimation:animation forKey:#"animatePath"];
[CATransaction commit];
}
Related
I am working on a circular progress view. I am adding slices or ARCs of a circle with unselected and selected colours that indicate progress of some task.
I am working in CALayers to draw Arcs and add animations. Everything is working fine for all slices with out gradient. If I add gradient in any slice / Arc it does not animate while drawing immediately.
Please help me in this issue to add animation in CAShapeLayer having CAGradientLayer.
Below is my code and my tried approaches...
NOTE: I have tried three approaches to add animation. Please check the commented code also
- (void)drawArcAnimation:(CGRect)rect {
if (self.itemIndex >= self.sliceItems.count) {
return;
}
float total = [self calculateTotal];
SliceItem *item = self.sliceItems[self.itemIndex];
UIBezierPath* path = [UIBezierPath bezierPath];
float angle = (item.itemValue/total) * 2 * M_PI;
float endAngle = self.startAngle + angle;
totalProgress += item.itemValue;
if (item.shouldGradient) {
// ***** This code will be used if we go for second approach of animation ****
UIBezierPath *rectToAnimateFrom = [UIBezierPath bezierPathWithArcCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2) radius:rect.size.width/2 - self.lineWidth startAngle:self.startAngle endAngle:self.startAngle clockwise:NO];
UIBezierPath *rectToAnimateTo = [UIBezierPath bezierPathWithArcCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2) radius:rect.size.width/2 - self.lineWidth startAngle:self.startAngle endAngle:endAngle clockwise:NO];
// ***** END ****
CGMutablePathRef arc = CGPathCreateMutable();
CGPathAddArc(arc, NULL,
(rect.size.width / 2), (rect.size.height / 2),
rect.size.width/2 - self.lineWidth,
self.startAngle,
endAngle,
NO);
self.startAngle = endAngle;
CGFloat lineWidth = self.lineWidth;
CGPathRef strokedArc =
CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
CAShapeLayer *segment = [CAShapeLayer layer];
segment.fillColor = item.itemColor.CGColor;
segment.strokeColor = [UIColor clearColor].CGColor;
segment.path = strokedArc;
// [self.baseLayer addSublayer:segment];
CAGradientLayer *gradient = [CAGradientLayer layer];
if (totalProgress > 50) {
gradient.colors = #[(id)item.itemSecondaryColor.CGColor, (id)item.itemColor.CGColor,(id)item.itemColor.CGColor,(id)item.itemColor.CGColor];
}else {
gradient.colors = #[(id)item.itemColor.CGColor, (id)item.itemColor.CGColor, (id)item.itemColor.CGColor,(id)item.itemSecondaryColor.CGColor];
}
gradient.frame = CGPathGetBoundingBox(segment.path);
CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
-CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
&translation);
gradient.mask = mask;
[self.baseLayer addSublayer:gradient];
if (_duration == 0.0) {
self.itemIndex++;
[self drawArcAnimation:rect];
}else {
// ******* First Approach to animate *******
[CATransaction begin];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = _duration*(angle/(2 * M_PI));
drawAnimation.repeatCount = 1.0;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[CATransaction setCompletionBlock:^{
self.itemIndex++;
[self drawArcAnimation:rect];
}];
[gradient addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
[CATransaction commit];
// ******* Second Approach to animate *******
// [CATransaction begin];
// CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
// animation.fromValue = (__bridge id _Nullable)(rectToAnimateFrom.CGPath);
// animation.toValue = (__bridge id _Nullable)(rectToAnimateTo.CGPath);
// animation.duration = 3;
// animation.repeatCount = 1;
// animation.removedOnCompletion = false;
// animation.fillMode = kCAFillModeForwards;
//
// [CATransaction setCompletionBlock:^{
// self.itemIndex++;
// [self drawArcAnimation:rect];
// }];
//
// [gradient addAnimation:animation forKey:#"fill animation"];
// [CATransaction commit];
// ******* Third Approach to animate *******
// NSArray *fromColors = #[(id)UIColor.whiteColor.CGColor, (id)UIColor.clearColor.CGColor];
// NSArray *toColors = gradient.colors;
// [gradient setColors:toColors];
//
// [CATransaction begin];
// CABasicAnimation *animation1 = [CABasicAnimation animationWithKeyPath:#"colors"];
// animation1.fromValue = fromColors;
// animation1.toValue = toColors;
// animation1.duration = _duration;
// animation1.removedOnCompletion = YES;
// animation1.fillMode = kCAFillModeForwards;
// animation1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// [CATransaction setCompletionBlock:^{
// self.itemIndex++;
// [self drawArcAnimation:rect];
// }];
// [gradient addAnimation:animation1 forKey:#"animateGradient"];
// [CATransaction commit];
}
}else {
[path addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:rect.size.width/2 - self.lineWidth
startAngle:self.startAngle
endAngle:endAngle
clockwise:YES];
self.startAngle = endAngle;
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
CGRect frame = path.bounds;
frame.size.height = rect.size.height;
frame.size.width = frame.size.width * 30;
layer.strokeColor = item.itemColor.CGColor;
layer.fillColor = nil;
layer.lineWidth = self.lineWidth;
layer.strokeStart = 0;
layer.strokeEnd = 1.0;
[self.baseLayer addSublayer:layer];
if (_duration == 0.0) {
self.itemIndex++;
[self drawArcAnimation:rect];
}else {
[CATransaction begin];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = _duration*(angle/(2 * M_PI));
animation.fromValue = #(0.0);
animation.toValue = #(1.0);
[CATransaction setCompletionBlock:^{
self.itemIndex++;
[self drawArcAnimation:rect];
}];
[layer addAnimation:animation forKey:nil];
[CATransaction commit];
}
}
}
In above code you can a condition if (item.shouldGradien) and its else part. Animations are working fine in else part of CALayers but no in gradient part.
What I am doing wrong here? Any help... ???
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
I am trying to animate the the appearance and then the disappearance of a UIBezierPath in a CAShapeLayer.
How I would like to do it is that at start, the path would be invisible by setting strokeStart and strokeEnd to 0. Then, in my animate method I would do a CABasicAnimation with setting the strokeEnd to 1, and after this is done, another animation where I set the strokeStart to 1, so the path would disappear again. Here is what I've tried:
- (void)animate
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
CABasicAnimation *appearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
appearingAnimation.duration = _duration / 2.0;
appearingAnimation.fromValue = #0;
appearingAnimation.toValue = #1;
CABasicAnimation *disappearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
disappearingAnimation.beginTime = _duration / 2.0;
disappearingAnimation.duration = _duration / 2.0;
disappearingAnimation.fromValue = #0;
disappearingAnimation.toValue = #1;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.duration = _duration;
animationGroup.animations = #[appearingAnimation, disappearingAnimation];
animationGroup.repeatCount = MAXFLOAT;
[layer addAnimation:animationGroup forKey:#"test"];
}
But the effect I am seeing is that the appearingAnimation is working properly, but as soon as it is done, the path instantly disappears without any animation. After this (_duration/2.0 time later) the whole thing starts over, so it would appear that the disappearingAnimation has no effect.
I tried another approach, but that only worked if the path was on my view.layer. As soon as I added it to a sublayer and tried to animate that one, the second (disappearing) animation had a glitch in it. Here is my second approach:
- (void)animate
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
layer.strokeStart = 0;
layer.strokeEnd = 1;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
layer.strokeStart = 1;
layer.strokeEnd = 1;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[self animate];
}];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
animation.duration = _duration / 2.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:1];
[layer addAnimation:animation forKey:#"animation1"];
[CATransaction commit];
}];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = _duration / 2.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:1];
[layer addAnimation:animation forKey:#"animation"];
[CATransaction commit];
}
Any idea why the first solution doesn't work at all, or why the second is only working properly on the view.layer and not on a sublayer?
When animating a layer property, you will want to set the layer property to the end value before adding the animation, as the animation changes are only applied to the presentation, not the model layer. Once the animation is completed it will be removed, leaving the layer in the state it was before.
Both the first and second methods you have outlined worked for me once a layer had been created (missing from your snippet):
- (void)animate
{
CAShapeLayer *layer = [CAShapeLayer layer];
CGFloat _duration = 2.0;
CGFloat radius = self.view.frame.size.width/2.0;
CGFloat startAngle = -(M_PI_2);
CGFloat endAngle = (3*M_PI_2);
CGPoint center = CGPointMake(self.view.frame.size.width/2.0, self.view.frame.size.height/2.0);
layer.path = [[UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES] CGPath];
layer.fillColor = [[UIColor clearColor] CGColor];
layer.strokeColor = [[UIColor blackColor] CGColor];
[self.view.layer addSublayer:layer];
... {animation code}
}
I think it is the problem of the value of _duration / 2.0. It may be the problem of floating number calculation.
Try not to halve the total duration but to make the durations of appearingAnimation and disappearingAnimation as the base duration:
#define DURATION_BASE 1
// You can choose the DURATION_BASE value whaterver you like
- (void)animate
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
CABasicAnimation *appearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
appearingAnimation.duration = DURATION_BASE;
appearingAnimation.fromValue = #0;
appearingAnimation.toValue = #1;
CABasicAnimation *disappearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
disappearingAnimation.beginTime = appearingAnimation.duration;
disappearingAnimation.duration = DURATION_BASE;
disappearingAnimation.fromValue = #0;
disappearingAnimation.toValue = #1;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.duration = appearingAnimation.duration+disappearingAnimation.duration;
animationGroup.animations = #[appearingAnimation, disappearingAnimation];
animationGroup.repeatCount = MAXFLOAT;
[layer addAnimation:animationGroup forKey:#"test"];
}
When you want to change durations of appearingAnimation and disappearingAnimation, you just need modify the code like
appearingAnimation.duration = DURATION_BASE*1.2;
disappearingAnimation.duration = DURATION_BASE*1.2;
I'm trying to create a circular bar such the one that you can find around the recording button in the native camera on iOS when you are doing a timelapse.
A circle is created along the animation, and once it is completed is removed again "naturally".
I tried next code:
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.lapseBtnOutlet.center.x, self.lapseBtnOutlet.center.y) radius:28 startAngle:2*M_PI*0-M_PI_2 endAngle:2*M_PI*1-M_PI_2 clockwise:YES].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor whiteColor].CGColor;
circle.lineWidth = 2.0;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = self.lapseInterval;
animation.removedOnCompletion = NO;
animation.fromValue = #(0);
animation.toValue = #(1);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[circle addAnimation:animation forKey:#"drawCircleAnimation"];
But my problem now is that I don't know how to "remove" the line from start-to-end.
I tried with autoreverse property, but it removes the circle from end-to-start instead from start-to-end. Any ideas?
You need to animate the strokeStart from 0 to 1, and when the animation finishes, remove the shape layer.
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:#selector(animateProperty:) withObject:#"strokeEnd" afterDelay:1];
[self performSelector:#selector(animateProperty:) withObject:#"strokeStart" afterDelay:3];
}
-(void)animateProperty:(NSString *) prop {
if (! self.circle) {
self.circle = [CAShapeLayer layer];
self.circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.lapseBtnOutlet.center.x, self.lapseBtnOutlet.center.y) radius:28 startAngle:2*M_PI*0-M_PI_2 endAngle:2*M_PI*1-M_PI_2 clockwise:YES].CGPath;
self.circle.fillColor = [UIColor clearColor].CGColor;
self.circle.strokeColor = [UIColor whiteColor].CGColor;
self.circle.lineWidth = 2.0;
[self.view.layer addSublayer:self.circle];
}
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:prop];
animation.delegate = ([prop isEqualToString:#"strokeStart"])? self : nil;
animation.duration = 1;
animation.removedOnCompletion = NO;
animation.fromValue = #0;
animation.toValue = #1;
[self.circle addAnimation:animation forKey:prop];
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
[self.circle removeFromSuperlayer];
self.circle = nil;
}
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