Rotating a CAShapeLayer arc about its center using CABasicAnimation - core-animation

I have an arc that I have drawn using CAShapeLayer and I want it to rotate around the circle's center. I am trying to get this animation using a CABasicAnimation on the 'transform.rotation' property. But the rotation seems is happening about a point that is not the same as the circle's center.
-(void)drawRect:(CGRect)rect{
int radius = 100;
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius].CGPath;
circle.fillColor = [UIColor orangeColor].CGColor;
circle.strokeColor = [UIColor blueColor].CGColor;
circle.lineWidth = 5;
circle.strokeStart = 0.0f;
circle.strokeEnd = 0.1f;
[self.layer addSublayer:circle];
CABasicAnimation *spinAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
spinAnimation.byValue = [NSNumber numberWithFloat:2.0f*M_PI];
spinAnimation.duration = 4;
spinAnimation.repeatCount = INFINITY;
[circle addAnimation:spinAnimation forKey:#"indeterminateAnimation"];
}
I know its an old problem, but whats the way out. I have tinkered a lot by setting the layer's bounds or the anchor point but it doesn't rotate about the center yet.
circle.bounds = self.frame;
Here is how I add the view to the view controller.
[self.view addSubview:[[ProgressIndicator alloc] initWithFrame:CGRectMake(50, 50, 200, 200)]];

This is what I eventually did.
The mistake was to add the animation to the sublayer and not the layer.
There is no need to tinker with anchorPoint or position here when the rotation is just needed about the center of the circle.
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
int radius = 50;
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor blueColor].CGColor;
circle.lineWidth = 5;
circle.strokeStart = 0.0f;
circle.strokeEnd = 0.1f;
[self.layer addSublayer:circle];
}
return self; }
- (void)drawRect:(CGRect)rect {
CABasicAnimation *spinAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
spinAnimation.byValue = [NSNumber numberWithFloat:2.0f*M_PI];
spinAnimation.duration = 4;
spinAnimation.repeatCount = INFINITY;
[self.layer addAnimation:spinAnimation forKey:#"indeterminateAnimation"]; }

Think is better to do something like this)
CGFloat center = CGRectGetWidth(frame) / 2;
CGFloat lineWidth = 5;
CGFloat radius = center - lineWidth / 2;
CGRect bounds = frame;
bounds.origin = CGPointZero;
CAShapeLayer *spinLayer = [CAShapeLayer layer];
spinLayer.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(center, center) radius:radius startAngle:0 endAngle:0.2 clockwise:YES].CGPath;
spinLayer.fillColor = [UIColor clearColor].CGColor;
spinLayer.strokeColor = [UIColor blueColor].CGColor;
spinLayer.lineWidth = 5;
spinLayer.lineCap = kCALineCapRound;
spinLayer.bounds = bounds;
spinLayer.position = CGPointMake(center, center);
[self.layer insertSublayer:spinLayer above:nil];
And animation:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.byValue = [NSNumber numberWithFloat:2.f * M_PI];
animation.duration = 1.5f;
animation.repeatCount = INFINITY;
[spinLayer addAnimation:animation forKey:#"lineRotation"];
[self setNeedsLayout];
Swift 3
var bounds = frame
bounds.origin = CGPoint.zero
let center: CGFloat = frame.size.width
let lineWidth: CGFloat = 5
let radius = center - lineWidth / 2
let spinLayer = CAShapeLayer()
spinLayer.path = UIBezierPath(arcCenter: CGPoint(x: center, y: center),
radius: radius,
startAngle: 0,
endAngle: 0.2,
clockwise: true).cgPath
spinLayer.strokeColor = UIColor.blue.cgColor
spinLayer.fillColor = UIColor.clear.cgColor
spinLayer.lineWidth = lineWidth
spinLayer.lineCap = kCALineCapRound
spinLayer.bounds = bounds
spinLayer.position = CGPoint(x: center, y: center)
view.layer.insertSublayer(spinLayer, above: nil)
let rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation.byValue = NSNumber(value: 2*M_PI as Double)
rotation.duration = 1
rotation.repeatCount = Float.infinity
spinLayer.add(rotation, forKey: "lineRotation")

Related

how to position a rotating CAShapeLayer at certain point in UIImageView?

I have the following code which makes an oval rotating around its center. I would like to be able to position it within UIImageView at a certain position (say a middle of the view).
I would really appreciate if someone could help me out on this one.
-(void)drawCircle
{
CGMutablePathRef circlePath = CGPathCreateMutable();
int radius = 400;
CGRect circleRect = CGRectMake(CENTER_Y-radius*0.7, CENTER_X-radius, 1.4*radius, 2*radius);
float midX = CGRectGetMidX(circleRect);
float midY = CGRectGetMidY(circleRect);
CGAffineTransform transform = CGAffineTransformMakeTranslation(-midX, -midY);
CGPathAddEllipseInRect( circlePath ,&transform , circleRect);
CAShapeLayer *circle = [[CAShapeLayer alloc] init];
circle.path = circlePath;
circle.opacity = 0.1;
circle.contentsScale = SCREEN_SCALE;
circle.fillColor = [UIColor blueColor].CGColor;
[aView.layer addSublayer:circle];
CABasicAnimation* theAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
theAnimation.fromValue = 0;
theAnimation.toValue = #(2*M_PI);
theAnimation.duration=3.5;
theAnimation.autoreverses=FALSE;
theAnimation.repeatCount=HUGE_VALF;
theAnimation.fillMode = kCAFillModeBoth;
theAnimation.removedOnCompletion=FALSE;
theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circle addAnimation:theAnimation forKey:#"transform.rotation.z"];
}
a missing piece:
[circle setPosition:CGPointMake(CENTER_X, CENTER_Y)];

Add a CAShapeLayer between annotation and MKMapView

I'm trying to add an animation below the annotations of my MKMapView but with no luck.
I tryed to change the CAShapeLayer zPosition but the circle end up below the map.
Here is my current code to add the animation to the map:
int radius = 55;
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor blackColor].CGColor;
circle.lineWidth = radius*2;
circle.opacity = 0.5;
[self.mapView.layer addSublayer:circle];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 2.5; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 100.0; // Animate only once..
drawAnimation.removedOnCompletion = YES; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
functionWithName:kCAMediaTimingFunctionEaseIn];
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
After 3 days without an answer I decided to remove my annotation from the mkview and put the animation and the image corresponding to the annotation in another view, above the map.

Circular Progress Bars in IOS

I want to create a circular progress bar like the following:
How can I do that using Objective-C and Cocoa?
How I started doing it was creating a UIView and editing the drawRect, but I am bit lost. Any help would be greatly appreciated.
Thanks!
The basic concept is to use the UIBezierPath class to your advantage. You are able to draw arcs, which achieve the effect you're after. I've only had half an hour or so to have a crack at this, but my attempt is below.
Very rudimentary, it simply uses a stroke on the path, but here we go. You can alter/modify this to your exact needs, but the logic to do the arc countdown will be very similar.
In the view class:
#interface TestView () {
CGFloat startAngle;
CGFloat endAngle;
}
#end
#implementation TestView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor whiteColor];
// Determine our start and stop angles for the arc (in radians)
startAngle = M_PI * 1.5;
endAngle = startAngle + (M_PI * 2);
}
return self;
}
- (void)drawRect:(CGRect)rect
{
// Display our percentage as a string
NSString* textContent = [NSString stringWithFormat:#"%d", self.percent];
UIBezierPath* bezierPath = [UIBezierPath bezierPath];
// Create our arc, with the correct angles
[bezierPath addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:130
startAngle:startAngle
endAngle:(endAngle - startAngle) * (_percent / 100.0) + startAngle
clockwise:YES];
// Set the display for the path, and stroke it
bezierPath.lineWidth = 20;
[[UIColor redColor] setStroke];
[bezierPath stroke];
// Text Drawing
CGRect textRect = CGRectMake((rect.size.width / 2.0) - 71/2.0, (rect.size.height / 2.0) - 45/2.0, 71, 45);
[[UIColor blackColor] setFill];
[textContent drawInRect: textRect withFont: [UIFont fontWithName: #"Helvetica-Bold" size: 42.5] lineBreakMode: NSLineBreakByWordWrapping alignment: NSTextAlignmentCenter];
}
For the view controller:
#interface ViewController () {
TestView* m_testView;
NSTimer* m_timer;
}
#end
- (void)viewDidLoad
{
// Init our view
[super viewDidLoad];
m_testView = [[TestView alloc] initWithFrame:self.view.bounds];
m_testView.percent = 100;
[self.view addSubview:m_testView];
}
- (void)viewDidAppear:(BOOL)animated
{
// Kick off a timer to count it down
m_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(decrementSpin) userInfo:nil repeats:YES];
}
- (void)decrementSpin
{
// If we can decrement our percentage, do so, and redraw the view
if (m_testView.percent > 0) {
m_testView.percent = m_testView.percent - 1;
[m_testView setNeedsDisplay];
}
else {
[m_timer invalidate];
m_timer = nil;
}
}
My example with magic numbers (for better understanding):
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];
I have implemented a simple library for iOS doing just that. It's based on the UILabel class so you can display whatever you want inside your progress bar, but you can also leave it empty.
Once initialized, you only have one line of code to set the progress :
[_myProgressLabel setProgress:(50/100))];
The library is named KAProgressLabel
You can check out my lib MBCircularProgressBar
For Swift use this,
let circle = UIView(frame: CGRectMake(0,0, 100, 100))
circle.layoutIfNeeded()
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius : CGFloat = circle.bounds.width / 2 * 0.83
var circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.5 * M_PI), endAngle: CGFloat(1.5 * M_PI), clockwise: true )
let progressCircle = CAShapeLayer()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.greenColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 1.5
progressCircle.strokeStart = 0
progressCircle.strokeEnd = 0.22
circle.layer.addSublayer(progressCircle)
self.view.addSubview(circle)
Reference: See Here.
Swift 3 use this,
CAShapeLayer with Animation : Continue with Zaid Pathan ans.
let circle = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
circle.layoutIfNeeded()
var progressCircle = CAShapeLayer()
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius : CGFloat = circle.bounds.width / 2 * 0.83
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.5 * M_PI), endAngle: CGFloat(1.5 * M_PI), clockwise: true )
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.cgPath
progressCircle.strokeColor = UIColor.green.cgColor
progressCircle.fillColor = UIColor.clear.cgColor
progressCircle.lineWidth = 2.5
progressCircle.strokeStart = 0
progressCircle.strokeEnd = 1.0
circle.layer.addSublayer(progressCircle)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1.0
animation.duration = 5.0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
progressCircle.add(animation, forKey: "ani")
self.view.addSubview(circle)
Here a Swift example of how to make a simple, not closed(to leave space for long numbers) circular progress bar with rounded corners and animation.
open_circular_progress_bar.jpg
func drawBackRingFittingInsideView(lineWidth: CGFloat, lineColor: UIColor) {
let halfSize:CGFloat = min( bounds.size.width/2, bounds.size.height/2)
let desiredLineWidth:CGFloat = lineWidth
let circle = CGFloat(Double.pi * 2)
let startAngle = CGFloat(circle * 0.1)
let endAngle = circle – startAngle
let circlePath = UIBezierPath(
arcCenter: CGPoint(x:halfSize, y:halfSize),
radius: CGFloat( halfSize – (desiredLineWidth/2) ),
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
let shapeBackLayer = CAShapeLayer()
shapeBackLayer.path = circlePath.cgPath
shapeBackLayer.fillColor = UIColor.clear.cgColor
shapeBackLayer.strokeColor = lineColor.cgColor
shapeBackLayer.lineWidth = desiredLineWidth
shapeBackLayer.lineCap = .round
layer.addSublayer(shapeBackLayer)
}
And the animation function.
func animateCircle(duration: TimeInterval) {
let animation = CABasicAnimation(keyPath: “strokeEnd”)
animation.duration = duration
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
shapeLayer.strokeEnd = 1.0
shapeLayer.add(animation, forKey: “animateCircle”)
}
There is a good blog with examples.

CABasicAnimation - Setting start stroke position

I'm animating the drawing of a basic circle. This works fine, except the animation begins drawing at the 3 o'clock position. Does anyone know how I can make it start at 12 o'clock?
self.circle = [CAShapeLayer layer];
self.circle.fillColor = nil;
self.circle.lineWidth = 7;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.bounds = CGRectMake(0, 0, 200, 200);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
[self.view.layer addSublayer:self.circle];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0;
drawAnimation.repeatCount = 1.0;
drawAnimation.removedOnCompletion = NO;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[self.circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
You can use bezierPathWithArcCenter instead of bezierPathWithOvalInRect, because that allows to specify a start and end angle:
CGFloat radius = self.circle.bounds.size.width/2; // Assuming that width == height
self.circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
radius:radius
startAngle:(-M_PI/2)
endAngle:(3*M_PI/2)
clockwise:YES].CGPath;
See the bezierPathWithArcCenter documentation for the meaning of the angles.

Rounder corners in UIView

i have a UIView and i would like to round it and add shadow color like this image :
For Rounder Corner
CAShapeLayer * maskLayer = [CAShapeLayer layer];
maskLayer.path = [UIBezierPath bezierPathWithRoundedRect: self.bounds byRoundingCorners: UIRectCornerBottomLeft | UIRectCornerTopRight cornerRadii: (CGSize){10.0, 10.}].CGPath;
self.layer.mask = maskLayer;
For Shadow
self.layer.masksToBounds = NO;
self.layer.shadowOffset = CGSizeMake(-15, 20);
self.layer.shadowRadius = 5;
self.layer.shadowOpacity = 0.5;
Hope this helps you!
To get the very same shadow as your image I would recommend you using a background image. Otherwise you should include <Quartzcore/Quartzcore.h> and use the following code:
view.layer.cornerRadius = 10;
view.frame = CGRectMake(15, 15, 100, 100);
view.backgroundColor = [UIColor redColor];
view.layer.shadowColor = [UIColor blackColor].CGColor;
view.layer.shadowOffset = CGSizeMake(2.0, 2.0);
view.layer.shadowOpacity = 0.8;
view.layer.shadowRadius = 10;
Some more information for the background image-option
Create a UIView that has the same width and height as the image including the shadow and assign the image to it this way:
view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"backgroundview.png"]];