I am working on animation of CAShapeLayer. I am drawing quarter circle(refer image) which is in 1st quadrant(as per my knowledge). Now I want to rotate it 90 degree so it should fix in 2nd quadrant with animation on single tap gesture. I tried it but it is not fixing into 2nd quadrant. I want to rotate around center of UIView.
Single tap gesture:
- (void)singleTapGestureCaptured:(UITapGestureRecognizer *)gesture {
CGPoint touchPoint=[gesture locationInView:self];
NSArray* subLayerArray = [self.layer sublayers];
for (int i = 0; i < subLayerArray.count; i++) {
CAShapeLayer* layer = [subLayerArray objectAtIndex:i];
if(CGPathContainsPoint(layer.path, NULL, touchPoint, NO)){
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, self.bounds.size.width/2-layer.frame.size.width/2, self.bounds.size.height/2-layer.frame.size.height/2, 100, 0*M_PI, M_PI, YES);
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = path;
anim.rotationMode = kCAAnimationRotateAuto;
anim.calculationMode = kCAAnimationPaced;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
anim.duration = 10.0;
[layer addAnimation:anim forKey:#""];
}
}
}
Drawing Code:
- (void)drawRect:(CGRect)rect {
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, rect.size.width/2, rect.size.height/2, 100, (0), (M_PI_2), NO);
CGPathAddArc(path, NULL, rect.size.width/2, rect.size.height/2, 100-50, (M_PI_2), (0), YES);
CGPathCloseSubpath(path);
CAShapeLayer* arcLayer = [[CAShapeLayer alloc]init];
arcLayer.path = path;
arcLayer.frame = CGPathGetPathBoundingBox(path);
arcLayer.fillColor = [UIColor yellowColor].CGColor;
arcLayer.bounds = CGPathGetPathBoundingBox(path);
[self.layer addSublayer:arcLayer];
}
and Image:
Related
I have created a subclass of CALayer to draw a slice of a pie chart with a custom color (color), a startAngle (actualStartAngle), an endAngle (actualEndAngle) and an image (image)[This image should always be in the middle of the slice] as animating this is not possible with a normal CAShapeLayer. The layer is currently drawn like this:
- (void)drawInContext:(CGContextRef)ctx{
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, self.center.x, self.center.y);
CGContextAddArc(ctx, self.center.x, self.center.y, self.radius, self.actualEndAngle, self.actualStartAngle, YES);
CGContextClosePath(ctx);
CGContextSetFillColorWithColor(ctx, self.color);
CGContextSetLineWidth(ctx, 0);
CGContextDrawPath(ctx, kCGPathFillStroke);
if (self.image) {
CGContextDrawImage(ctx, [self getFrameForImgLayerWithStartAngle:self.actualStartAngle andEndAngle:self.actualEndAngle], self.image);
}
}
These individual slices are animated by:
[CATransaction begin];
CABasicAnimation *colorAnimation = [CABasicAnimation animationWithKeyPath:#"color"];
colorAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
colorAnimation.duration = duration;
colorAnimation.fromValue = (id)self.color;
colorAnimation.toValue = (id)colorTU.CGColor;
CABasicAnimation *startAngleAnimation = [CABasicAnimation animationWithKeyPath:#"actualStartAngle"];
startAngleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
startAngleAnimation.duration = duration;
startAngleAnimation.fromValue = [self valueForKey:#"actualStartAngle"];
startAngleAnimation.toValue = [[NSNumber alloc] initWithFloat:newStartAngle];
CABasicAnimation *endAngleAnimation = [CABasicAnimation animationWithKeyPath:#"actualEndAngle"];
endAngleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
endAngleAnimation.duration = duration;
endAngleAnimation.fromValue = [self valueForKey:#"actualEndAngle"];
endAngleAnimation.toValue = [[NSNumber alloc] initWithFloat:newEndAngle];
if (completionBlock) {
[CATransaction setCompletionBlock:completionBlock];
}
if (colorTU && colorTU.CGColor != self.color) {
[self addAnimation:colorAnimation forKey:#"color"];
}
if (newStartAngle != self.actualStartAngle) {
[self addAnimation:startAngleAnimation forKey:#"actualStartAngle"];
}
if (newEndAngle != self.actualEndAngle) {
[self addAnimation:endAngleAnimation forKey:#"actualEndAngle"];
}
[CATransaction commit];
The problem is that I would like to have the image only take up the space that the individual layer takes up (resp. the shape in it). This is solved in general by this method:
- (BOOL)imageFitsIntoSliceWithStartAngle:(float)sA andEndAngle:(float)eA{
BOOL fits = YES;
CGRect rectToUse = [self getFrameForImgLayerWithStartAngle:sA andEndAngle:eA];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:self.center];
[path addArcWithCenter:self.center radius:self.deg startAngle:sA endAngle:eA clockwise:YES];
for (int w = 0; w <= 1; w++) {
if (fits) {
for (int h = 0; h <= 1; h++) {
if (![path containsPoint:CGPointMake(rectToUse.origin.x + rectToUse.size.width * w, rectToUse.origin.y + rectToUse.size.height * h)]) {
fits = NO;
break;
}
}
}
}
return fits;
}
The problem now is that it may fit in the end and not at the beginning of the animation, so I would have to cut the image to be as big as the slice when this happens.
I have tried the following:
set a mask for the slice instead of drawing it directly -> This is firstly bad coding and I have found it to be very energy inefficient, also it created an error (expecting model layer not copy) for me.
Any ideas on how to solve this issue? (not what I tried but this problem in general, I don't think you should do something like what I tried above)
Thanks
Don't know if it'll help, but here the drawing code for a pie chart part :
- (void)drawChartWithFrame:(CGRect)frame startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle fillColor:(UIColor *)fillColor strokeColor:(UIColor *)strokeColor
{
//// Oval Drawing
CGRect ovalRect = CGRectMake(CGRectGetMinX(frame),CGRectGetMinY(frame), CGRectGetWidth(frame), CGRectGetHeight(frame));
UIBezierPath* ovalPath = UIBezierPath.bezierPath;
[ovalPath addArcWithCenter: CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)) radius: CGRectGetWidth(ovalRect) / 2 startAngle: -(startAngle - 25) * M_PI/180 endAngle: -(endAngle - 93) * M_PI/180 clockwise: YES];
[ovalPath addLineToPoint: CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))];
[ovalPath closePath];ansform ovalTransform = CGAffineTransformMakeTranslation(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect));
ovalTransform = CGAffineTransformScale(ovalTransform, 1, CGRectGetHeight(ovalRect) / CGRectGetWidth(ovalRect));
[ovalPath applyTransform: ovalTransform];
[fillColor setFill];
[ovalPath fill];
[strokeColor setStroke];
ovalPath.lineWidth = 1;
[ovalPath stroke];
}
It's much lighter to animate that a real image.
The code below is drawing me a circle, how can I modify the existing code to draw a triangle instead?
_colorDotLayer = [CALayer layer];
CGFloat width = self.bounds.size.width-6;
_colorDotLayer.bounds = CGRectMake(0, 0, width, width);
_colorDotLayer.allowsGroupOpacity = YES;
_colorDotLayer.backgroundColor = self.annotationColor.CGColor;
_colorDotLayer.cornerRadius = width/2;
_colorDotLayer.position = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
While there are shown several Core Graphics solution, I want to add a Core Animation based solution.
- (void)viewDidLoad
{
[super viewDidLoad];
UIBezierPath* trianglePath = [UIBezierPath bezierPath];
[trianglePath moveToPoint:CGPointMake(0, view3.frame.size.height-100)];
[trianglePath addLineToPoint:CGPointMake(view3.frame.size.width/2,100)];
[trianglePath addLineToPoint:CGPointMake(view3.frame.size.width, view2.frame.size.height)];
[trianglePath closePath];
CAShapeLayer *triangleMaskLayer = [CAShapeLayer layer];
[triangleMaskLayer setPath:trianglePath.CGPath];
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,0, size.width, size.height)];
view.backgroundColor = [UIColor colorWithWhite:.75 alpha:1];
view.layer.mask = triangleMaskLayer;
[self.view addSubview:view];
}
code based on my blog post.
#implementation TriangleView {
CAShapeLayer *_colorDotLayer;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (_colorDotLayer == nil) {
_colorDotLayer = [CAShapeLayer layer];
_colorDotLayer.fillColor = self.annotationColor.CGColor;
[self.layer addSublayer:_colorDotLayer];
}
CGRect bounds = self.bounds;
CGFloat radius = (bounds.size.width - 6) / 2;
CGFloat a = radius * sqrt((CGFloat)3.0) / 2;
CGFloat b = radius / 2;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, -radius)];
[path addLineToPoint:CGPointMake(a, b)];
[path addLineToPoint:CGPointMake(-a, b)];
[path closePath];
[path applyTransform:CGAffineTransformMakeTranslation(CGRectGetMidX(bounds), CGRectGetMidY(bounds))];
_colorDotLayer.path = path.CGPath;
}
- (void)awakeFromNib {
[super awakeFromNib];
self.annotationColor = [UIColor redColor];
}
#end
Result:
I think is more easy than this solutions:
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:(CGPoint){20, 0}];
[path addLineToPoint:(CGPoint){40, 40}];
[path addLineToPoint:(CGPoint){0, 40}];
[path addLineToPoint:(CGPoint){20, 0}];
// Create a CAShapeLayer with this triangular path
// Same size as the original imageView
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = self.viewTriangleCallout.bounds;
mask.path = path.CGPath;
This is my white triangle:
You can change points or rotate if you want:
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:(CGPoint){0, 0}];
[path addLineToPoint:(CGPoint){40, 0}];
[path addLineToPoint:(CGPoint){20, 20}];
[path addLineToPoint:(CGPoint){0, 0}];
// Create a CAShapeLayer with this triangular path
// Same size as the original imageView
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = self.viewTriangleCallout.bounds;
mask.path = path.CGPath;
Example code, it is based on this SO Answer which draws stars:
#implementation TriangleView
-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
int sides = 3;
double size = 100.0;
CGPoint center = CGPointMake(160.0, 100.0);
double radius = size / 2.0;
double theta = 2.0 * M_PI / sides;
CGContextMoveToPoint(context, center.x, center.y-radius);
for (NSUInteger k=1; k<sides; k++) {
float x = radius * sin(k * theta);
float y = radius * cos(k * theta);
CGContextAddLineToPoint(context, center.x+x, center.y-y);
}
CGContextClosePath(context);
CGContextFillPath(context); // Choose for a filled triangle
// CGContextSetLineWidth(context, 2); // Choose for a unfilled triangle
// CGContextStrokePath(context); // Choose for a unfilled triangle
}
#end
Use UIBezeierPaths
CGFloat radius = 20;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, (center.x + bottomLeft.x) / 2, (center.y + bottomLeft.y) / 2);
CGPathAddArcToPoint(path, NULL, bottomLeft.x, bottomLeft.y, bottomRight.x, bottomRight.y, radius);
CGPathAddArcToPoint(path, NULL, bottomRight.x, bottomRight.y, center.x, center.y, radius);
CGPathAddArcToPoint(path, NULL, center.x, center.y, bottomLeft.x, bottomLeft.y, radius);
CGPathCloseSubpath(path);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:path];
CGPathRelease(path);
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
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"];
}
I need to draw complex shape, something like this:
So I try:
CGFloat offsetAngle = 15 * M_PI/180;
CGPathAddArc(path, nil, 39/2, 30, 39/2-0.1, offsetAngle, -M_PI-offsetAngle, YES);
CGFloat linesXOffset = 2.5;
CGFloat linesYOffset = 30;
CGPathMoveToPoint(path, nil, linesXOffset, linesYOffset);
CGPathAddLineToPoint(path, nil, 39-linesXOffset, linesYOffset);
CGPathAddLineToPoint(path, nil,39/2,//X
linesYOffset + CGRectGetHeight(self.bounds)/2-220/2-linesYOffset);
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
[shapeLayer setPath:path];
So shape consists of triangle and arc.
But I have the following overlay problem:
Is it possible to fix?
1) Easier:
CGFloat linesYOffset = 35;
2) Another way:
CGFloat offsetAngle = 15 * M_PI/180;
CGMutablePathRef path1 = CGPathCreateMutable();
CGPathAddArc(path1, nil, 39/2, 30, 39/2-0.1, offsetAngle, -M_PI-offsetAngle, YES);
CGMutablePathRef path2 = CGPathCreateMutable();
CGFloat linesXOffset = 5.5;
CGFloat linesYOffset = 30;
CGPathMoveToPoint(path2, nil, linesXOffset, linesYOffset);
CGPathAddLineToPoint(path2, nil, 39-linesXOffset, linesYOffset);
CGPathAddLineToPoint(path2, nil,39/2,//X
linesYOffset + 600/2-220/2-linesYOffset);
CAShapeLayer *shapeLayer1 = [CAShapeLayer layer];
[shapeLayer1 setPath:path1];
CAShapeLayer *shapeLayer2 = [CAShapeLayer layer];
[shapeLayer2 setPath:path2];
CAShapeLayer* container = [CAShapeLayer layer];
[container addSublayer:shapeLayer1];
[container addSublayer:shapeLayer2];