I have this need to display two points emitting signals. The way the signals is represented is the wifi signal strength indicator that we commonly see in Macs.
However, there are a couple of changes to that:
It is inverted. The source is pointed at top. Look at the image below:
And I need to animate the lines to give the indication that the source is emitting signal.
I was able to get the attached images by overriding the drawRect of the View class:
CGContext context = UIGraphics.GetCurrentContext();
context.SetLineWidth(2.0f);
UIColor color = UIColor.FromRGB(65, 137, 77);
context.SetStrokeColorWithColor(color.CGColor);
float maxRadius = rect.Height;
const float delta = 15;
float Y = center.Y;
for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= delta) {
context.AddArc(center.X, Y, currentRadius, -ToRadians(startAngle), -ToRadians(endAngle), true);
context.StrokePath();
}
I'm out of my depths here.
If someone can point me to the right direction, that would be super awesome!
I took a shot at this using Core Animation. I just add a shape layer with a circle shape for each of the waves and then dynamically adjust their strokeStart and strokeEnd properties to get it to vary the width of each wave. So the output is like this:
Here is the initialization code:
- (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(300.0f, 100.0f);
waveLayer.strokeColor = [[UIColor lightGrayColor] CGColor];
waveLayer.fillColor = [[UIColor clearColor] CGColor];
waveLayer.lineWidth = 5.0f;
waveLayer.path = circlePath.CGPath;
// Translate on the y axis to shift all the layers down while
// we're creating them. Could do this with the position value as well
// but this is a little cleaner.
waveLayer.transform = CATransform3DMakeTranslation(0.0f, i*25, 0.0f);
// strokeStart begins at 3:00. You would need to transform (rotate)
// the layer 90 deg CCW to have it start at 12:00
waveLayer.strokeStart = 0.25 - ((i+1) * 0.01f);
waveLayer.strokeEnd = 0.25 + ((i+1) * 0.01f);
[self.view.layer addSublayer:waveLayer];
}
}
And then here is where I add the animations for each layer:
- (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"];
}
}
I posted it to github. Not sure if it's exactly what you're looking for, but hopefully it points you in the right direction.
Related
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);
Some year ago i have used this code for realize a chronometer animation.
Was a cute animation but doesn't work with iOS8>
A circle should appears and lose a pie every 0.1sec but The animation doesn't start. It works on my iPhone 5 iOS 7.1
I tried to solve it but after 2 hours i have no solution.
Can someone with more experience with CABasicAnimation help me?
THANK YOU.
This is the code:
//Animazione CRONOMETRO
-(void) startCronometro{
//SetTime
counterStart = TIME;
[sfondoCronometro setImage:[UIImage imageNamed:#"cronoStart.png"]];
maskLayer = [CAShapeLayer layer];
CGFloat maskHeight = sfondoCronometro.frame.size.height;
CGFloat maskWidth = sfondoCronometro.frame.size.width;
CGPoint centerPoint;
centerPoint = CGPointMake(sfondoCronometro.frame.size.width/2, (sfondoCronometro.frame.size.height/2));
//Make the radius of our arc large enough to reach into the corners of the image view.
CGFloat radius = sqrtf(maskWidth * maskWidth + maskHeight * maskHeight)/2;
//Don't fill the path, but stroke it in black.
maskLayer.fillColor = [[UIColor clearColor] CGColor];
maskLayer.strokeColor = [[UIColor blackColor] CGColor];
maskLayer.lineWidth = 25;
CGMutablePathRef arcPath = CGPathCreateMutable();
//Move to the starting point of the arc so there is no initial line connecting to the arc
CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y-radius/2);
//Create an arc at 1/2 our circle radius, with a line thickess of the full circle radius
CGPathAddArc(arcPath, nil, centerPoint.x, centerPoint.y, radius/2, 3*M_PI/2, -M_PI/2, NO);
maskLayer.path = arcPath;//[aPath CGPath];//arcPath;
//Start with an empty mask path (draw 0% of the arc)
maskLayer.strokeEnd = 0;
CFRelease(arcPath);
//Install the mask layer into out image view's layer.
sfondoCronometro.layer.mask = maskLayer;
//Set our mask layer's frame to the parent layer's bounds.
sfondoCronometro.layer.mask.frame = sfondoCronometro.layer.bounds;
//Create an animation that increases the stroke length to 1, then reverses it back to zero.
CABasicAnimation *swipe = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
swipe.duration = TIME;
NSLog(#"TIME: %f", swipe.duration);
swipe.delegate = self;
// [swipe setValue:#"string" forKey:#"key"];
swipe.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
swipe.fillMode = kCAFillModeForwards;
swipe.removedOnCompletion = NO;
swipe.toValue = [NSNumber numberWithFloat: 1.0];
[maskLayer addAnimation: swipe forKey: #"strokeEnd"];
}
I seem to remember that at some point the function of CGPathAddArc changed, and depending on the values of your start and end angle, you had to flip the clockwise flag on the call. Try using clockwise = YES.
I just did a little testing in an app of mine and confirmed this. For the angles you're using, clockwise NO works for iOS <= 7, but fails for iOS >=8.
Switch the last parameter from NO to YES.
I'm trying to create circle animation by adding CAShapeLayers with different stroke colors:
UIView *view = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
[self.view addSubview:view];
view.backgroundColor = [UIColor redColor];
[self addCircleAnimationTo:view direction:YES];
- (void)addCircleAnimationTo:(UIView *)view direction:(BOOL)direction {
CGFloat animationTime = 3;
// Set up the shape of the circle
int radius = view.frame.size.width/2;
CAShapeLayer *circle = [CAShapeLayer layer];
// Make a circular shape
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
// Center the shape in view
circle.position = CGPointMake(radius,
radius);
circle.bounds = view.bounds;
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
// switch color
circle.strokeColor = (direction) ? [UIColor blueColor].CGColor : [UIColor whiteColor].CGColor;
circle.lineWidth = 5;
circle.lineJoin = kCALineJoinBevel;
circle.strokeStart = .0;
circle.strokeEnd = 1.;
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = animationTime;
drawAnimation.repeatCount = 1;
// Animate from no part of the stroke being drawn to the entire stroke being drawn
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Add to parent layer
[view.layer addSublayer:circle];
// Add the animation to the circle
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((animationTime-0.1) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self addCircleAnimationTo:view direction:!direction];
});
}
I'm wondering why I can see blue when white layer animates and how to get rid of this weird blue color border:
You know that you are adding more and more shape layers after each call to addCircleAnimationTo ?
While you animate the new layer the old shape layer reminds below the new one. That is why you see it.
[EDIT 2] SHORT EXPLANATION
We create two layers first with blue path colour, second with white path colour.
To first layer we add fill animation - change of layer.strokeEnd property, we animate it from 0 to 1 and from 1 to 1. Half of duration from 0 to 1 and half of duration from 1 to 1 (visually nothing happens, but is needed).
We add clear animation - change of layer.strokeStart, we animate it from 0 to 1. Duration of this animation is half of duration of fill animation. Begin time is also half of fill animation because we want to move beginning of stroke when end stroke is still.
We add the same animations to second, white layer but with appropriate begin time offset, thanks to that white path unwinds and blue path winds.
[EDIT] ADDED REAL ANSWER
Here is my suggestion to solve your animation problem. Hope it helps ... :)
- (void)viewDidLoad {
[super viewDidLoad];
UIView *view = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
[self.view addSubview:view];
view.backgroundColor = [UIColor redColor];
self.circle1 = [self circle:view.bounds];
self.circle1.strokeColor = [UIColor blueColor].CGColor;
self.circle2 = [self circle:view.bounds];
self.circle2.strokeColor = [UIColor whiteColor].CGColor;
[view.layer addSublayer:self.circle1];
[view.layer addSublayer:self.circle2];
[self.circle1 addAnimation:[self fillClearAnimation:0] forKey:#"fillclear"];
[self.circle2 addAnimation:[self fillClearAnimation:3] forKey:#"fillclear"];
}
- (CAAnimationGroup *)fillClearAnimation:(CFTimeInterval)offset
{
CGFloat animationTime = 3;
CAAnimationGroup *group = [CAAnimationGroup animation];
CAKeyframeAnimation *fill = [CAKeyframeAnimation animationWithKeyPath:#"strokeEnd"];
fill.values = #[ #0,#1,#1];
fill.duration = 2*animationTime;
fill.beginTime = 0.f;
CAKeyframeAnimation *clear = [CAKeyframeAnimation animationWithKeyPath:#"strokeStart"];
clear.values = #[ #0, #1];
clear.beginTime = animationTime;
clear.duration = animationTime;
group.animations = #[ fill, clear ];
group.duration = 2*animationTime;
group.beginTime = CACurrentMediaTime() + offset;
group.repeatCount = FLT_MAX;
return group;
}
- (CAShapeLayer *)circle:(CGRect)frame
{
CAShapeLayer *circle = [CAShapeLayer layer];
CGFloat radius = ceil(frame.size.width/2);
// Make a circular shape
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
// Center the shape in view
circle.position = CGPointMake(radius,
radius);
circle.frame = frame;
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
circle.lineWidth = 5;
circle.lineJoin = kCALineJoinBevel;
circle.strokeStart = 0.f;
circle.strokeEnd = 0.f;
return circle;
}
The antialiasing is what's causing the problem
When the white CAShapeLayer gets drawn over the blue, the edge gets blended with the previous edge blending that the antialiasing created.
The only real solution is to clear the blue from underneath the white as you animate. You can do this by animating the strokeStart property of the previous layer from 0.0 to 1.0. You should be able to re-use your drawAnimation for this, by just changing the keyPath. For example, change your dispatch_after to:
- (void)addCircleAnimationTo:(UIView *)view direction:(BOOL)direction {
// Insert your previous animation code, where drawAnimation is defined and added to the circle layer.
drawAnimation.keyPath = #"strokeStart"; // Re-use the draw animation object, setting it to animate strokeStart now.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((animationTime-0.1) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self addCircleAnimationTo:view direction:!direction];
[circle addAnimation:drawAnimation forKey:#"(un)drawCircleAnimation"]; // Sets the previous layer to animate out
});
}
That way the antialiasing edge blending gets cleared from the blue layer, allowing the white layer to get a nice & clean antialiasing. Just don't forget to remove the layer once you've animated it out!
Another potential solution is to increase the stroke width of the white layer, so it strokes over the blue layer's edge blending, however I would imagine it would create an undesired effect. You could also disable antialiasing, but that would look horrible on the circle.
I have a circular CAShapeLayer that I'd like to animate the fill of. I have created my CAShapeLayer as
- (void)viewDidLoad {
[super viewDidLoad];
// Filled layer
CAShapeLayer *filledCircleShape = [CAShapeLayer new];
filledCircleShape.frame = CGRectMake(100, 100, 100, 100);
filledCircleShape.fillColor = [UIColor purpleColor].CGColor;
filledCircleShape.strokeColor = [UIColor grayColor].CGColor;
filledCircleShape.lineWidth = 2;
filledCircleShape.path = [self filledBezierPathWithRelativeFill:.75 inFrame:filledCircleShape.frame].CGPath;
[self.view.layer addSublayer:filledCircleShape];
self.filleShapeLayer = filledCircleShape;
}
- (UIBezierPath *)filledBezierPathWithRelativeFill:(CGFloat)relativeFill
{
UIBezierPath *filledCircleArc = [UIBezierPath bezierPath];
CGFloat arcAngle = 2 * acos(1 - 2 * relativeFill); // 2arccos ((r - 2rp) / r) == 2 arccos(1 - 2p)
CGFloat startAngle = (M_PI - arcAngle) / 2;
CGFloat endAngle = startAngle + arcAngle;
[filledCircleArc addArcWithCenter:self.boundsCenter radius:self.radius startAngle:startAngle endAngle:endAngle clockwise:YES];
return filledCircleArc;
}
This looks like this:
I have tried the following to make it animate a path change.
- (void)animate
{
UIBezierPath *toBezierPath = [self filledBezierPathWithRelativeFill:0.3 inFrame:CGRectMake(100, 100, 100, 100)];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnimation.duration = 5.f;
pathAnimation.toValue = (__bridge id)toBezierPath.CGPath;
[self.filleShapeLayer addAnimation:pathAnimation forKey:#"path"];
}
which kind of works. The shape layer animates but looks like this
I want the shape layer to animate like an hour glass, starting at one percentage and then animates down to another.
Any ideas of how I can achieve this using a CAShapeLayer?
The CAShapeLayer is overkill for the fill. It isn't magically going to animate the way you want, and it isn't necessary anyway. Just start with a big square purple view and animate it downwards — but put a mask so that only the interior of the shape region is visible — as I do here (except that I'm filling instead of emptying):
I have a CABasicAnimation that creates an iris wipe effect on an image. In short, the animation works fine on the simulator but there is no joy on device. The timers still fire correctly and the animationCompleted block gets called however there is no visible animation.
Here is the code to get the iris wipe working:
- (void)irisWipe
{
animationCompletionBlock theBlock;
_resultsImage.hidden = FALSE;//Show the image view
[_resultsImage setImage:[UIImage imageNamed:#"logoNoBoarder"]];
[_resultsImage setBackgroundColor:[UIColor clearColor]];
[_resultsImage setFrame:_imageView.bounds];
//Create a shape layer that we will use as a mask for the waretoLogoLarge image view
CAShapeLayer *maskLayer = [CAShapeLayer layer];
CGFloat maskHeight = _resultsImage.layer.bounds.size.height;
CGFloat maskWidth = _resultsImage.layer.bounds.size.width;
CGPoint centerPoint;
centerPoint = CGPointMake( maskWidth/2, maskHeight/2);
//Make the radius of our arc large enough to reach into the corners of the image view.
CGFloat radius = sqrtf(maskWidth * maskWidth + maskHeight * maskHeight)/2;
// CGFloat radius = MIN(maskWidth, maskHeight)/2;
//Don't fill the path, but stroke it in black.
maskLayer.fillColor = [[UIColor clearColor] CGColor];
maskLayer.strokeColor = [[UIColor blackColor] CGColor];
maskLayer.lineWidth = radius; //Make the line thick enough to completely fill the circle we're drawing
// maskLayer.lineWidth = 10; //Make the line thick enough to completely fill the circle we're drawing
CGMutablePathRef arcPath = CGPathCreateMutable();
//Move to the starting point of the arc so there is no initial line connecting to the arc
CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y-radius/2);
//Create an arc at 1/2 our circle radius, with a line thickess of the full circle radius
CGPathAddArc(arcPath,
nil,
centerPoint.x,
centerPoint.y,
radius/2,
3*M_PI/2,
-M_PI/2,
NO);
maskLayer.path = arcPath;
//Start with an empty mask path (draw 0% of the arc)
maskLayer.strokeEnd = 1.0;
CFRelease(arcPath);
//Install the mask layer into out image view's layer.
_resultsImage.layer.mask = maskLayer;
//Set our mask layer's frame to the parent layer's bounds.
_resultsImage.layer.mask.frame = _resultsImage.layer.bounds;
//Create an animation that increases the stroke length to 1, then reverses it back to zero.
CABasicAnimation *swipe = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
swipe.duration = 1;
swipe.delegate = self;
[swipe setValue: theBlock forKey: kAnimationCompletionBlock];
swipe.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
swipe.fillMode = kCAFillModeForwards;
swipe.removedOnCompletion = NO;
swipe.autoreverses = NO;
swipe.toValue = [NSNumber numberWithFloat: 0];
//Set up a completion block that will be called once the animation is completed.
theBlock = ^void(void)
{
NSLog(#"completed");
};
[swipe setValue: theBlock forKey: kAnimationCompletionBlock];
// doingMaskAnimation = TRUE;
[maskLayer addAnimation: swipe forKey: #"strokeEnd"];
}
Is there something in iOS7 I should be aware of when working with CAAnimations etc? OR is there a error in the code?
Note this code was sourced from: How do you achieve a "clock wipe"/ radial wipe effect in iOS?
I think the problem (or at least part of it) may be this line:
[_resultsImage setFrame:_imageView.bounds];
That should read
[_resultsImage setBounds:_imageView.bounds];
Instead. If you set the FRAME to the bounds of the image view, you're going to move the image view to 0.0 in its superview.
I'm also not clear what _imageView is, as opposed to _resultsImage.
I would step through your code in the debugger, looking at the frame rectangles that are being set for the image view and mask layer, and all the other values that are calculated.