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.
Related
I am using this code to create the slide to unlock like animation but I am unable to make it animate from right to left.
How can I make it start from right and animate to left?
-(void)slideToCancel {
CALayer *maskLayer = [CALayer layer];
// Mask image ends with 0.15 opacity on both sides. Set the background color of the layer
// to the same value so the layer can extend the mask image.
maskLayer.backgroundColor = [[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.50f] CGColor];
maskLayer.contents = (id)[[UIImage imageNamed:#"slidetocancel.png"] CGImage];
// Center the mask image on twice the width of the text layer, so it starts to the left
// of the text layer and moves to its right when we translate it by width.
maskLayer.contentsGravity = kCAGravityCenter;
maskLayer.frame = CGRectMake(_slideToCancelLbl.frame.size.width * -1, 0.0f, _slideToCancelLbl.frame.size.width * 2, _slideToCancelLbl.frame.size.height);
// Animate the mask layer's horizontal position
CABasicAnimation *maskAnim = [CABasicAnimation animationWithKeyPath:#"position.x"];
maskAnim.byValue = [NSNumber numberWithFloat:_slideToCancelLbl.frame.size.width];
maskAnim.repeatCount = 1e100f;
maskAnim.duration = 1.5f;
[maskLayer addAnimation:maskAnim forKey:#"slideAnim"];
_slideToCancelLbl.layer.mask = maskLayer;
}
You should be able to simply use a negative byValue. However, the code you've posted is a canned animation, not one that responds to a slide gesture.
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 am using Swift 1.2 and my goal is to animate an image mask over a static UIImage. What I have implemented is a swift version of masking an image that I originally found in Objective-C.
func maskImage(image: UIImage, mask: UIImage) -> UIImage! {
let maskRef = mask.CGImage;
let mask = CGImageMaskCreate(
CGImageGetWidth(maskRef),
CGImageGetHeight(maskRef),
CGImageGetBitsPerComponent(maskRef),
CGImageGetBitsPerPixel(maskRef),
CGImageGetBytesPerRow(maskRef),
CGImageGetDataProvider(maskRef), nil, false);
let masked = CGImageCreateWithMask(image.CGImage, mask);
let retImage = UIImage(CGImage: masked);
return retImage;
}
It works great! However, putting it in motion is my challenge.
Is there a way to either iteratively apply the mask with a different horizontal offset or a better way to approach this problem entirely - perhaps with a CALayer implementation?
Thanks for your help!
EDIT: Based on what was posted as an answer, I added this:
let image = UIImage(named: "clouds");
let imageView = UIImageView(image: image);
let layer = CALayer();
layer.contents = UIImage(named: "alpha-mask")?.CGImage;
layer.bounds = CGRectMake(0, 0, image.size.width, image.size.height);
// For other folks learning, this did not work
//let animation = CABasicAnimation(keyPath: "bounds.origin.x");
// This does work
let animation = CABasicAnimation(keyPath: "position.x");
animation.duration = 2;
animation.delegate = self;
animation.fillMode = kCAFillModeForwards;
animation.repeatCount = 0;
animation.fromValue = 0.0;
animation.toValue = image.size.width;
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear);
animation.removedOnCompletion = false;
layer.addAnimation(animation, forKey: "transform");
imageView.layer.mask = layer;
self.addSubview(imageView);
I am able to see the alpha mask properly, but the animation does not work. Any ideas?
EDIT: I modified the code above and it works! I needed to make the keyPath position.x. See above
You do indeed want to use a CALayer - or rather, a CAShapeLayer.
You can create a CAShapeLayer and install it as as the mask on another layer.
You can create a CAAnimation that animates changes to the shape layer's path, or you can animate changes to the layer's strokeStart and/or strokeEnd properties.
If you animate the path, the one rule you want to follow is to make sure that the starting and ending path have the same number and type of control points. Otherwise the animation is "undefined", and the results can be very strange.
I have a development blog post that outlines how it's done:
http://wareto.com/using-core-animation-groups-to-create-animation-sequences-2
It's primarily about using CAAnimationGroups, but it also includes a working example of animating changes to a CAShapeLayer that's used as the mask of an image view's layer.
Below is a GIF of the mask animation that it creates - a "clock wipe" that shows and hides an image view:
Unfortunately it's written in Objective-C, but the Core Animation calls are nearly identical in Swift. Let me know if you have any problems figuring out how to adapt it.
The meat of the animation code is this method:
- (IBAction)doMaskAnimation:(id)sender;
{
waretoLogoLarge.hidden = FALSE;//Show the image view
//Create a shape layer that we will use as a mask for the waretoLogoLarge image view
CAShapeLayer *maskLayer = [CAShapeLayer layer];
CGFloat maskHeight = waretoLogoLarge.layer.bounds.size.height;
CGFloat maskWidth = waretoLogoLarge.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;
//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
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,
YES);
maskLayer.path = arcPath;
//Start with an empty mask path (draw 0% of the arc)
maskLayer.strokeEnd = 0.0;
CFRelease(arcPath);
//Install the mask layer into out image view's layer.
waretoLogoLarge.layer.mask = maskLayer;
//Set our mask layer's frame to the parent layer's bounds.
waretoLogoLarge.layer.mask.frame = waretoLogoLarge.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 = 2;
swipe.delegate = self;
[swipe setValue: theBlock forKey: kAnimationCompletionBlock];
swipe.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
swipe.fillMode = kCAFillModeForwards;
swipe.removedOnCompletion = NO;
swipe.autoreverses = YES;
swipe.toValue = [NSNumber numberWithFloat: 1.0];
[maskLayer addAnimation: swipe forKey: #"strokeEnd"];
}
I have another blog entry that IS in Swift that shows how to create and animate a pie chart using a CAShapeLayer. That project animates shape, not a mask, but the only real difference is whether you install the shape layer as a regular content layer or as a mask on another layer like the backing layer of an image view.
You can check out that project at this link:
http://wareto.com/swift-piecharts
This question already has an answer here:
Animation Snaps back when the UIImage is changed
(1 answer)
Closed 8 years ago.
I'm trying to build a simple pie-chart style progress indicator. It animates well, but after the animation concludes, it snaps right back to the previous value before the animation.
My code is as follows:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
// Create a white ring that fills the entire frame and is 2 points wide.
// Its frame is inset 1 point to fit for the 2 point stroke width
CGFloat radius = CGRectGetWidth(self.frame) / 2;
CGFloat inset = 1;
CAShapeLayer *ring = [CAShapeLayer layer];
ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
ring.fillColor = [UIColor clearColor].CGColor;
ring.strokeColor = [UIColor whiteColor].CGColor;
ring.lineWidth = 2;
// Create a white pie-chart-like shape inside the white ring (above).
// The outside of the shape should be inside the ring, therefore the
// frame needs to be inset radius/2 (for its outside to be on
// the outside of the ring) + 2 (to be 2 points in).
self.innerPie = [CAShapeLayer layer];
inset = radius/2; // The inset is updated here
self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
self.innerPie.fillColor = [UIColor clearColor].CGColor;
self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
self.innerPie.lineWidth = (radius-inset)*2;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
self.innerPie.hidden = YES;
}
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
self.innerPie.hidden = NO;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
pathAnimation.duration = 3.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:_progress];
pathAnimation.toValue = [NSNumber numberWithFloat:progress];
[self.innerPie addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
if (_progress != progress) {
_progress = progress;
}
}
I simply set it up in drawRect then have a method to set the progress with an animation. What am I doing wrong here?
That's how core Animation works. What you want to do is to set the property to it's final value just before adding the animation to the layer. Then the animation hides the fact that you changed the property, and when the animation is removed after it's finished, the property is at it's end value.