I have a UIView that draws a circle inside it using the following code:
-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, myColor);
CGContextFillEllipseInRest(context, myRect);
}
I want to animate this circle to a grey color... How can I achieve this?
-(void)fadeToGrey {
// What goes here?
}
You should use CAShapeLayer as a layer class and do your drawing inside setLayerProperties: method. drawRect: will not be overridden. Here is my sample code with comments of custom UIView with attaching color animation method.
//Returns the class used to create the layer for instances of this class.
//it is CALayer class by default.
+ (Class)layerClass
{
return [CAShapeLayer class];
}
//Lays out subviews.
- (void)layoutSubviews
{
[self setLayerProperties];
}
- (void)setLayerProperties {
//The view’s Core Animation layer used for rendering.
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
// Color Declarations
UIColor* strokeColor = [UIColor redColor];
// Some sample Bezier Drawing. In my case it is a triangle.
// However, you can draw circle by using `+bezierPathWithRoundedRect:cornerRadius:`
UIBezierPath* bezierPath = [UIBezierPath bezierPath];
[bezierPath moveToPoint: CGPointMake(0.0, self.frame.size.height)];
[bezierPath addLineToPoint: CGPointMake(self.frame.size.width/2, 0.0)];
[bezierPath addLineToPoint: CGPointMake(CGRectGetWidth(self.frame), CGRectGetHeight(self.frame))];
[bezierPath addLineToPoint: CGPointMake(0.0, CGRectGetHeight(self.frame))];
bezierPath.miterLimit = 8;
bezierPath.lineJoinStyle = kCGLineJoinBevel;
layer.path = bezierPath.CGPath;
layer.fillColor = strokeColor.CGColor;
}
//here is the magic :)
- (void)attachColorAnimationToColor:(UIColor *)color {
//"fillColor" property of CAShapeLayer's instance is animatable
//(thus we have overridden `layerClass` method btw), that leads us to create
//CABasicAnimation instance with fromValue (initial color), toValue (final color).
CABasicAnimation *animation = [self animationWithKeyPath:#"fillColor"];
animation.fromValue = (__bridge id)(((CAShapeLayer *)self.layer).fillColor);
animation.toValue = (__bridge id)color.CGColor;
[self.layer addAnimation:animation forKey:animation.keyPath];
}
//setting properties of color animation
- (CABasicAnimation *)animationWithKeyPath:(NSString *)keyPath {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion=NO;
animation.repeatCount = 1;
animation.duration = 0.3f;
return animation;
}
You will use it by calling attachColorAnimationToColor: like this:
//your custom view declaration
YourView *yourView = [[YourView alloc]initWithFrame:CGRectMake(...)];
[self.view addSubview:yourView];
[yourView attachColorAnimationToColor:[UIColor lightGrayColor]];
CAShape Layer Class Reference to fillColor property
Feel free to ask if something goes wrong.
UPDATE
Our layer fillColor is red. So if you fire animation to redColor, that means that you want animation from 'red' to 'red'. Create method reverse animation, where toValue is our stroke color of shape. (Animation does not update it, it stays red always). Here is sample that was tested now :)
-(void)attachReverseAnimationFromColor:(UIColor*)color{
CABasicAnimation *animation = [self animationWithKeyPath:#"fillColor"];
animation.toValue = (__bridge id)(((CAShapeLayer *)self.layer).fillColor);
animation.fromValue = (__bridge id)color.CGColor;
[self.layer addAnimation:animation forKey:animation.keyPath];
}
PS Happy Coding.
Related
I have an one view with horizontal and vertical beizer paths.The resulting view looks like the foloowing
Now on touch of the view if user has touched the one of the path,I have to change the color of that path by animation that means on touch of a path ,path's storke color will be changed animaticllay.
I am able to do it with the normal way ,that means when user touch the path I am able to change the color but not with the animation effect.
I have created the sub class of UIBeizerPath with properties like defaultStorkeColor,SelectedStorkeColor and I am manipulating them in the drawrect.
My drawrect is
-(void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
for(BCBeizerPath * path in pathsArray)
{
if(path.selected)
{
[path.selectedStrokeColor set];
}
else
{
[path.defaultStrokeColor set];
}
[path stroke];
}
}
Please help me in implementing that.
I didn't try this code but hit with a same scenario before, which I had resolved by doing something like the below:
In -drawRect draw these patterns over a CAShapeLayer using UIBezierPath and add it to your CustomView's layer
[self.layer addSubLayer:self.shapeLayer_main];
Then get the frame of the rect you wish to draw the stroke using
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchLocation = [touch locationInView:self];
//Check in which rect in your view contains the touch location
//Then call the method to draw the stroke with the detected rect as the parameter
[self drawStrokeWithRect:touchedRect];
}
In the method,
-(void)drawStrokeWithRect:(CGRect )touchedRect {
//Create a shapeLayer with desired strokeColor, lineWidth and path(A UIBezierPath)
//This is the layer your are going to animate
//Add this shapeLayer to your self.shapeLayer_main added to the customView's layer
CAShapeLayer * shapeLayer = [CAShapeLayer layer];
shapeLayer.lineWidth = 4;
shapeLayer.strokeColor = strokeColor.CGColor;
shapeLayer.fillColor = transparentColor.CGColor;
shapeLayer.path = [[UIBezierPath bezierPathWithRect:touchedRect]CGPath];
[self.shapeLayer_main addSublayer:shapeLayer];
//Adding Animation to the ShapeLayer
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 1.0;
drawAnimation.removedOnCompletion = NO;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[shapeLayer addAnimation:drawAnimation forKey:#"StrokeAnimation"];
}
Every time you tap the view, a rect is drawn with the stroke color you gave with the animation.
I'm trying to animate the drawing of a line by the following way:
.h
CAShapeLayer *rootLayer;
CAShapeLayer *lineLayer;
CGMutablePathRef path;
.m
path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, self.frame.size.width/2-100, 260);
CGPathAddLineToPoint(path, nil, self.frame.size.width/2+100.0, 260);
CGPathCloseSubpath(path);
self.rootLayer = [CALayer layer];
rootLayer.frame = self.bounds;
[self.layer addSublayer:rootLayer];
self.lineLayer = [CAShapeLayer layer];
[lineLayer setPath:path];
[lineLayer setFillColor:[UIColor redColor].CGColor];
[lineLayer setStrokeColor:[UIColor blueColor].CGColor];
[lineLayer setLineWidth:1.5];
[lineLayer setFillRule:kCAFillRuleNonZero];
[rootLayer addSublayer:lineLayer];
[self performSelector:#selector(startTotalLine) withObject:nil afterDelay:1.5];
- (void)startTotalLine
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"animatePath"];
[animation setDuration:3.5];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animation setAutoreverses:NO];
[animation setFromValue:(id)path];
[animation setToValue:(id)path];
[lineLayer addAnimation:animation forKey:#"animatePath"];
}
The line had drawn before the startTotalLine method is invoked.
Also, the startTotalLine method doesn't affect the line.
I want it to animate the the line drawing from right to left.
I would do it with an animated property.
To achieve this I would create a custom CALayer class - let's call it LineLayer. Define a startPoint property, and a length property. I'd then configure the length property to be "animatable".
The code for that would look something like the following:
// LineLayer.h
#interface LineLayer: CALayer
#property (nonatomic, assign) int length;
// This was omitted from the SO code snippet.
#property (nonatomic, assign) CGPoint startPoint;
#end
// LineLayer.m
#implementation LineLayer
#synthesize length = _length;
// This was omitted from the SO code snippet.
#synthesize startPoint= _startPoint;
- (id) initWithLayer:(id)layer
{
if(self = [super initWithLayer:layer])
{
if([layer isKindOfClass:[LineLayer class]])
{
// This bit is required for when we CA is interpolating the values.
LineLayer *other = (LineLayer*)layer;
self.length = other.length;
self.startPoint = other.startPoint; // This was omitted.
}
}
return self;
}
+ (BOOL)needsDisplayForKey:(NSString *)key
{
if ([key isEqualToString:#"length"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
- (void) setLength:(int)newLength
{
if (newLength < 0) {
return; // Fail early.
}
_length = newLength;
[self setNeedsDisplay];
}
/*
This should have been drawInContext:(CGContextRef)context
- (void) drawRect:(CGRect) rect
*/
- (void) drawInContext:(CGContextRef)context
{
//...Do your regular drawing here.
// This was omitted from the SO code snippet.
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2);
CGContextMoveToPoint(context, _startPoint.x, _startPoint.y);
CGContextAddLineToPoint(context, _startPoint.x + _length, _startPoint.y);
CGContextStrokePath(context);
}
#end
Then in your view controller you could use LineLayer like this:
- (void)viewDidLoad
{
[super viewDidLoad];
LineLayer *lineLayer = [LineLayer new];
// This was omitted from the SO code snippet.
lineLayer.frame = CGRectMake(0, 0, 320, 480);
[lineLayer setNeedsDisplay];
// ---
lineLayer.startPoint = CGPointMake(0, 100);
lineLayer.length = 0;
[self.view.layer addSublayer:lineLayer];
// Now animate the changes to the length property
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"length"];
anim.duration = 5; // Change should table about 5 mins.
anim.fromValue = [NSNumber numberWithInt:0];
anim.toValue = [NSNumber numberWithInt:200];
[lineLayer addAnimation:anim forKey:#"animateLength"];
lineLayer.length = 200;
//Do clean up below...
}
Happy Coding :)
I think the easiest way to do what you want, is to present some UIView that is 1.5 pixel height and animate it's width. Ask me if I'm not clear.
I think your code doesn't work because your variable path is not a layer property. Read manuals:
CABasicAnimation provides basic, single-keyframe animation
capabilities for a layer property.
And you do something strange here:
[animation setFromValue:(id)path];
[animation setToValue:(id)path];
EDIT:
I stumbled upon an article, and understood what you were try to achieve! Now I think the reason you failed is that you can animate path that doesn't change number of points. And now I thought you can create a path-line with two points. At first they are at same place and another path is the line you want to end up with. Now animate from first path to the second. I think it should work, but I'm not sure.
EDIT:
Definitely! You need this guy's code. Git link.
Here is my solution for your case using "UIBezierPath", "CAShapeLayer" and the property "strokeEnd":
.m file
#synthesize shapeLayer;
[self drawLine];
-(void)drawLine {
CGFloat X1 = self.frame.size.width/2-100;
CGFloat Y1 = 260;
CGFloat X2 = self.frame.size.width/2+100.0;
CGFloat Y2 = 260;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(X1, Y1)];
[path addLineToPoint:CGPointMake(X2, Y2)];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = [[UIColor blueColor] CGColor];
shapeLayer.lineWidth = 1.5;
shapeLayer.fillColor = [[UIColor redColor] CGColor];
shapeLayer.strokeEnd =0;
[self.layer addSublayer:shapeLayer];
[self performSelector:#selector(startTotalLine) withObject:nil afterDelay:1.5];
}
- (void)startTotalLine {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 3.5f;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animation setAutoreverses:NO];
[animation setFromValue:[NSNumber numberWithInt:0]];
[animation setToValue:[NSNumber numberWithInt:1]];
[shapeLayer addAnimation:animation forKey:#"animatePath"];
}
The default "strokeEnd" value is 1, so, make it 0 at the beginning (no line will appear). Btw, here is useful examples provided: http://jamesonquave.com/blog/fun-with-cashapelayer/
I am messing around with a basic CAAnimation example where Im just trying to get a CALayer to rotate in a subclass of UIView. I start by making a new CALayer with a red background and inserting it into self.layer. I then make a CAAnimation that is supposed to be triggered on opacity changes and apply it to the CALayer, after which it spins as expected.
Weird thing is that if I try to apply the exact same animation using performSelector:withObject:afterDelay:, the CALayer no longer animates, although its opacity still changes. Even weirder is that if I use performSelector:withObject:afterDelay: to animate self.layer using pretty much the same code, it works.
Any ideas as to what's going on? Here is my code:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
point = [[CAGradientLayer alloc] init];
point.backgroundColor = [UIColor redColor].CGColor;
point.bounds = CGRectMake(0.0f, 0.0f, 30.0f, 20.0f);
point.position = CGPointMake(100.0f, 100.0f);
[self.layer addSublayer:point];
[point release];
[self performSelector:#selector(test) withObject:nil afterDelay:2];
}
return self;
}
-(void)test {
[self spinLayer:point];
// [self spinLayer:self.layer]; works here
}
-(CAAnimation*)animationForSpinning {
CATransform3D transform;
transform = CATransform3DMakeRotation(M_PI/2.0, 0, 0, 1.0);
CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
basicAnimation.toValue = [NSValue valueWithCATransform3D:transform];
basicAnimation.duration = 5;
basicAnimation.cumulative = NO;
basicAnimation.repeatCount = 10000;
return basicAnimation;
}
-(void)spinLayer:(CALayer*)layer {
CAAnimation *anim = [self animationForSpinning];
[layer addAnimation:anim forKey:#"opacity"];
layer.opacity = 0.6;
}
and this is my interface
#interface MyView : UIView {
CALayer *point;
}
-(void)test;
-(void)spinLayer:(CALayer*)layer;
-(CAAnimation*)animationForSpinning;
#end
Note: I am instantiating this view with a frame equal to [UIScreen mainScreen].applicationFrame from within the app delegate.
Change this:
[layer addAnimation:anim forKey:#"opacity"];
into this:
[layer addAnimation:anim
forKey:#"notOpacity!"];
What happens is that implicit animation layer.opacity = 0.6 overwrites you explicit animation. This happens because Core Animation uses property names when adding implicit animations.
(If you move layer.opacity = 0.6 above [layer addAnimation:anim forKey:#"opacity"]; it will also work, but layer.opacity will not get animated.)
And why it works without delay: because implicit animations are "enabled" only if layer is in the tree and been displayed.
I want to animate the drawing of a path as it is being drawn on the screen I am assuming Core Animation is the best way with a NSBezierPath however I am not sure on how to implement this.
Basically the idea is to have a path from point A to point B slowly being animated from A to B until the path reaches B. A good tutorial on how to do this would be great.
You can create CAShapeLayer with your CGPath (can be created from UIBezierPath for example).
Then path property of CAShapeLayer is animatable itself and you can also create more advanced animations using animatable strokeStart and strokeEnd properties (available starting iOS 4.2)
Simple example of how to add layer with random line that appears with animation:
CAShapeLayer *l = [CAShapeLayer layer];
l.frame = self.view.bounds;
l.strokeColor = [UIColor redColor].CGColor;
CGPoint start = CGPointMake(arc4random()%300+10, arc4random()%400+40);
CGPoint end = CGPointMake(arc4random()%300+10, arc4random()%400+40);
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:start];
[path addLineToPoint:end];
l.path = path.CGPath;
[path release];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
animation.duration = 3.0f;
[l addAnimation:animation forKey:#"myStroke"];
[self.view.layer addSublayer:l];
I'm trying to slide down an image in an UIImageView, but I don't know which combination of UIContentMode and animation property is the right one to make that happen.
The image should always have the same size and should not be streched... all I want is, that nothing is visible first and then the frame extends and reveals the image.
Maybe it's easier if you see what I mean:
So, it sounds rather easy, but what UIContentMode should I use for the UIImageView and what property should I animate? Thank you!
I took your lead and made a screencast as well. Was this what you had in mind?
I put the animation repeating indefinitely so it would be easier to capture with a video, but it can be started at the press of a button, as well as frozen in place, showing the popover and its contents, until reversed to be hidden again.
I used Core Animation for that, instead of animating a UIView, since I wanted to use the mask property of CALayer to hide the popover and reveal it with a sliding animation.
Here is the code I used (same as in the video):
- (void)viewDidLoad
{
// Declaring the popover layer
CALayer *popover = [CALayer layer];
CGFloat popoverHeight = 64.0f;
CGFloat popoverWidth = 200.0f;
popover.frame = CGRectMake(50.0f, 100.0f, popoverWidth, popoverHeight);
popover.contents = (id) [UIImage imageNamed:#"popover.png"].CGImage;
// Declaring the mask layer
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = CGRectMake(0, - popoverHeight, popoverWidth, popoverHeight);
maskLayer.backgroundColor = [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f].CGColor;
// Setting the animation (animates the mask layer)
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
animation.byValue = [NSNumber numberWithFloat:popoverHeight];
animation.repeatCount = HUGE_VALF;
animation.duration = 2.0f;
[maskLayer addAnimation:animation forKey:#"position.y"];
//Assigning the animated maskLayer to the mask property of your popover
popover.mask = maskLayer;
[self.view.layer addSublayer:popover];
[super viewDidLoad];
}
NOTE: You have to import the QuartzCore framework into your project and write this line in your header file: #import <QuartzCore/QuartzCore.h>.
Tells if this works for you or if you need any more help setting this up.
Try this code.
Consider the UIImageView as imageView.
imageView.contentMode = UIViewContentModeScaleAspectFill;
CGRect imageRect = imageView.frame;
CGRect origImgRect = imageRect;
imageRect.size.height = 5;
imageView.frame = imageRect;
[UIView animateWithDuration:2.0
animations:^{imageView.rect = origImgRect;}
completion:^(BOOL finished){ }];