Making a gradient arc? - objective-c

i wanna make an arc with the use of CAGradientLayer in such a way:-
20% of the arc have same single color then other 80% of arc has a gradient of two colors.
I already tried by hand on locations
startPoint endPoint property of CAGradientLayer but couldn't get the success and already go through from tutorials but somehow couldn't understand the concept properly.please solve my problem with clear description of it.
// for beizier path
- (UIBezierPath *)samplePath
{
UIBezierPath *path = [UIBezierPath bezierPath];
path = [UIBezierPath bezierPath];
[path addArcWithCenter:CGPointMake(200, 200) radius:30.0f startAngle:(3*M_PI)/4 endAngle:M_PI/4 clockwise:YES];
path.lineWidth = 30;
[[UIColor redColor] setStroke];
// [[UIColor colorWithRed:arc4random() green:arc4random() blue:arc4random() alpha:1.0] setFill];
[path stroke];
return path;
}
- (void)startAnimation
{
CAShapeLayer *shapeLayer;
if (self.pathLayer == nil)
{
shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [[self samplePath] CGPath];
shapeLayer.strokeColor = [[UIColor redColor] CGColor];
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 30;
self.pathLayer = shapeLayer;
}
[self animationBasic];
[self gradientLayer:shapeLayer];
}
-(void)animationBasic
{
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 3.0;
pathAnimation.fromValue = #(0.0f);
pathAnimation.toValue = #(1.0f);
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
-(void)gradientLayer:(CAShapeLayer *)shapelayer
{
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.frame;
gradientLayer.colors = #[(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor redColor].CGColor ];
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.5f],
[NSNumber numberWithFloat:1.0f],
nil];
// gradientLayer.startPoint = CGPointMake(0,1);
// gradientLayer.endPoint = CGPointMake(1,1);
[self.layer addSublayer:gradientLayer];
gradientLayer.mask = shapelayer;
}

The locations property of a CAGradientLayer instance should be populated with the starting points (I know, the word stops makes it sound the opposite) of each color as specified in the colors property, from 0 to 1. These stops are rendered vertically from top to bottom.
So, this should work:
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:0.5f],
nil];
Now, at the same time, if you want to make the gradient horizontal (or any other direction), you can make use of the startPoint and endPoint properties.
gradientLayer.startPoint = CGPointMake(0.0, 0.5); //Default is (0.5, 0,0)
gradientLayer.endPoint = CGPointMake(1.0, 0.5); //Default is (0.5, 1.0)
//The gradientLayer will be rendered horizontally.
The startingPoint and endingPoint properties determine the direction and layout of the gradient in the layer. These are defined in the unit coordinate system, where each point is defined as a set of unit coordinates and the top left corner being the origin. So, (0,0) corresponds to the origin, (1,1) corresponds to the bottom right corner. You can check this excellent answer for a better explanation.
So to summarize, the gradient is rendered in the direction and region as specified by the startPoint and endPoint properties, with the colors rendered in the ratio specified in the locations property.
At the end, you can only understand this fully by tinkering with the properties and using various combinations to see what effect is rendered.

Related

Objective c 2D gradient

I need to make at 2D gradient. I have the first part of it make a gradient vertical direction from a color to a clear color:
+ (void)addFadeout:(UIView *) view withColor:(UIColor *)color {
UIColor * halfClearColor = [color colorWithAlphaComponent:0];
CALayer *layer = view.layer;
layer.masksToBounds = YES;
CAGradientLayer *shineLayer = [CAGradientLayer layer];
shineLayer.frame = layer.bounds;
shineLayer.colors = #[
(id)color.CGColor,
(id)halfClearColor.CGColor];
shineLayer.locations = #[
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:1.0f]];
[view.layer insertSublayer:shineLayer atIndex:0];
}
But I also need to be able to add multiple colors in the horizontal direction where they either fade together and is shown separately(still with the vertical gradient). I know I could just add them besides each other but that is not what I am looking for.. How would you do it?
It might be something like this but I can't seem to connect the parts:
http://cupsofcocoa.com/tag/gradient/
https://developer.apple.com/library/mac/#documentation/graphicsimaging/conceptual/drawingwithquartz2d/dq_shadings/dq_shadings.html
Edit 1:
This is what I got now:
It might be unclear what I wanted. I also want a gradient along the x-axis between multiple colors. So what I got combined with something like this: (to replace the red color)
Solution:
+ (void)addFadeout:(UIView *) view withColors:(NSArray *)colors { //CGColors
UIColor * someColor = [UIColor colorWithCGColor:((__bridge CGColorRef)[colors lastObject])];//only alpha channel used with more than one color
UIColor * halfClearColor = [someColor colorWithAlphaComponent:0];
CALayer *layer = view.layer;
layer.masksToBounds = YES;
CAGradientLayer *transparencyLayer = [CAGradientLayer layer];
transparencyLayer.frame = layer.bounds;
transparencyLayer.colors = #[
(id)someColor.CGColor,
(id)halfClearColor.CGColor];
transparencyLayer.locations = #[
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:1.0f]];
if(colors.count > 1){
CAGradientLayer * multiColoredLayer = [self getMultiColoredLayerWithColors:colors inLayer:layer];
multiColoredLayer.mask = transparencyLayer;
[view.layer insertSublayer:multiColoredLayer atIndex:0];
}
else{
[view.layer insertSublayer:transparencyLayer atIndex:0];
}
}
+ (CAGradientLayer *)getMultiColoredLayerWithColors:(NSArray *)colors inLayer:(CALayer *)layer{ //CGColors
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = layer.bounds;
gradientLayer.colors = colors;
gradientLayer.startPoint = CGPointMake(0, 0.5);
gradientLayer.endPoint = CGPointMake(1.0, 0.5);
return gradientLayer;
}
You need to use 2 of 'something' as gradients can only be drawn in a single direction at a time. You could do it with 2 CAGradientLayers, one behind the other (or one as a sublayer of the other). Or, you could do it with 2 CGGradients, both drawn into the same context.
From your comment, you want to alpha mask. The best way to apply the 'top' layer with transparency is by setting it as the mask of the 'bottom' layer:
CAGradientLayer *colorLayer = ...;
CAGradientLayer *transparencyLayer = ...;
colorLayer.mask = transparencyLayer;
In this case, any colour in the transparencyLayer is ignored and only the alpha values are used.

Objective C - Faded Gradient on left/right sides of UICollectionView

I have a horizontal-scrolling UICollectionView within the main view of a view controller like so (Grey is UIView, Wood is UICollectionView):
I want to add fixed faded gradients on the far left & far right sies of this UICollectionView so that the scrolls appear to vanish as the user scrolls. How would I go about doing this? Does it involve some use of CAGradientLayer? I would be grateful for any help you can give me!
I actually managed to figure this out using one mask layer thanks to this tutorial at cocoanetics. Here's what I did:
#interface ScalesViewController : UIViewController
{
CAGradientLayer *maskLayer;
}
#end
Then in the .m, I placed in the following:
- (void)viewDidAppear:(BOOL)animated
{
[super viewWillAppear: animated];
if (!maskLayer)
{
maskLayer = [CAGradientLayer layer];
CGColorRef outerColor = [[UIColor colorWithWhite:0.0 alpha:1.0] CGColor];
CGColorRef innerColor = [[UIColor colorWithWhite:0.0 alpha:0.0] CGColor];
maskLayer.colors = [NSArray arrayWithObjects:
(__bridge id)outerColor,
(__bridge id)innerColor,
(__bridge id)innerColor,
(__bridge id)outerColor, nil];
maskLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.125],
[NSNumber numberWithFloat:0.875],
[NSNumber numberWithFloat:1.0], nil];
[maskLayer setStartPoint:CGPointMake(0, 0.5)];
[maskLayer setEndPoint:CGPointMake(1, 0.5)];
maskLayer.bounds = self.mainCollectionView.bounds;
maskLayer.anchorPoint = CGPointZero;
[self.mainCollectionView.layer insertSublayer: maskLayer atIndex: 0];
}
}
This creates a nice "fade to black" effect on both sides of my collection view. More colors can be added to the locations & color properties to refine the gradient blend. The start/endpoints determine the direction and location of the gradient.
Try to add two layers of CAGradientLayer on the collection view sublayer:
#import <QuartzCore/QuartzCore.h>
CAGradientLayer *leftShadow = [CAGradientLayer layer];
leftShadow.frame = CGRectMake(0, 0, 100, self.collectionView.frame.size.height);
leftShadow.startPoint = CGPointMake(0, 0.5);
leftShadow.endPoint = CGPointMake(1.0, 0.5);
leftShadow.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:0.0 alpha:0.4f] CGColor], (id)[[UIColor clearColor] CGColor], nil];
[self.collectionView.layer addSublayer:leftShadow];
CAGradientLayer *rightShadow = [CAGradientLayer layer];
rightShadow.frame = CGRectMake(CGRectGetWidth(self.collectionView.frame)-100.0, 0, 100, self.collectionView.frame.size.height);
rightShadow.startPoint = CGPointMake(1.0, 0.5);
rightShadow.endPoint = CGPointMake(0, 0.5);
rightShadow.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:0.0 alpha:0.4f] CGColor], (id)[[UIColor clearColor] CGColor], nil];
[self.collectionView.layer addSublayer:rightShadow];

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.

Animating CAShapeLayer size change

I am drawing a circle, with an initial radius of 200
self.circle = [CAShapeLayer layer];
self.circle.fillColor = nil;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 7;
self.circle.bounds = CGRectMake(0, 0, 2 * radius, 2 * radius);
self.circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.radius, self.radius)
Can anyone please tell me how to animate a change to a radius of 100?
This is how I ended up doing it:
UIBezierPath *newPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(newRadius, newRadius) radius:newRadius startAngle:(-M_PI/2) endAngle:(3*M_PI/2) clockwise:YES];
CGRect newBounds = CGRectMake(0, 0, 2 * newRadius, 2 * newRadius);
CABasicAnimation* pathAnim = [CABasicAnimation animationWithKeyPath: #"path"];
pathAnim.toValue = (id)newPath.CGPath;
CABasicAnimation* boundsAnim = [CABasicAnimation animationWithKeyPath: #"bounds"];
boundsAnim.toValue = [NSValue valueWithCGRect:newBounds];
CAAnimationGroup *anims = [CAAnimationGroup animation];
anims.animations = [NSArray arrayWithObjects:pathAnim, boundsAnim, nil];
anims.removedOnCompletion = NO;
anims.duration = 2.0f;
anims.fillMode = kCAFillModeForwards;
[self.circle addAnimation:anims forKey:nil];
This is, I believe, the simpler and preferred way to do it:
UIBezierPath *newPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(newRadius, newRadius) radius:newRadius startAngle:(-M_PI/2) endAngle:(3*M_PI/2) clockwise:YES];
CABasicAnimation* pathAnim = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnim.fromValue = (id)self.circle.path;
pathAnim.toValue = (id)newPath.CGPath;
pathAnim.duration = 2.0f;
[self.circle addAnimation:pathAnim forKey:#"animateRadius"];
self.circle.path = newPath.CGPath;
I got this approach from the Apple Documentation here (See listing 3-2). The important things here are that you're setting the fromValue and also setting the final value for self.circle's path to newPath.CGPath right after you set up the animation. An animation does not change the underlying value of the model, so in the other approach, you're not actually making a permanent change to the shape layer's path variable.
I've used a solution like the chosen answer in the past (using removedOnCompletion and fillMode etc) but I found that on certain versions of iOS they caused memory leaks, and also sometimes that approach just doesn't work for some reason.
With this approach, you're creating the animation explicitly (both where it starts from and where it ends), and you're specifying the final permanent value for the path at the end (with that last line), so that there's no need to mess with not removing the animation when it finishes (which I believe is what leads to memory leaks if you do this animation a lot of times... the unremoved animations build up in memory).
Also, I removed the animation of the bounds because you don't need it to animate the circle. Even if the path is drawn outside the bounds of the layer, it'll still be fully shown (see the docs for CAShapeLayer about that). If you do need to animate the bounds for some other reason, you can use the same approach (making sure to specify the fromValue and the final value for bounds).
You can use a CAAnimationGroup to animate the bounds and path of your CAShapeLayer:
- (void)animateToRadius:(CGFloat)newRadius {
CAShapeLayer *shapeLayer = ...;
UIBezierPath *newBezierPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(newRadius, newRadius)];
NSValue *newBounds = [NSValue valueWithCGRect:CGRectMake(0,0,2*newRadius,2*newRadius);
CAAnimationGroup *group = [CAAnimationGroup animation];
NSMutableArray *animations = [#[] mutableCopy];
NSDictionary *animationData = #{#"path":newBezierPath.CGPath, #"bounds":newBounds};
for(NSString *keyPath in animationData) {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
animation.toValue = animationData[keyPath];
[animations addObject:animation];
}
group.animations = [animations copy];
group.duration = 2;
group.removedOnCompletion = NO;
group.fillMode = kCAFillModeForwards;
[shapeLayer addAnimation:group forKey:nil];
}
Here is a direct swift conversion of Jonathan's code should anyone need it.
func scaleShape(shape : CAShapeLayer){
let newRadius : CGFloat = 50;
let newPath: UIBezierPath = UIBezierPath(arcCenter: CGPointMake(newRadius, newRadius), radius: newRadius, startAngle: (CGFloat(-M_PI) / 2.0), endAngle: (3 * CGFloat(M_PI) / 2), clockwise: true);
let newBounds : CGRect = CGRect(x: 0, y: 0, width: 2 * newRadius, height: 2 * newRadius);
let pathAnim : CABasicAnimation = CABasicAnimation(keyPath: "path");
pathAnim.toValue = newPath.CGPath;
let boundsAnim : CABasicAnimation = CABasicAnimation(keyPath: "bounds");
boundsAnim.toValue = NSValue(CGRect: newBounds);
let anims: CAAnimationGroup = CAAnimationGroup();
anims.animations = [pathAnim, boundsAnim];
anims.removedOnCompletion = false;
anims.duration = 2.0;
anims.fillMode = kCAFillModeForwards;
shape.addAnimation(anims, forKey: nil);
}

Animating CAShapeLayer's strokeColor with CABasicAnimation

I've been unsuccessful at animating a flashing stroke on a CAShapeLayer using the answer from this previous thread, and after many searches I can find no other examples of animating the stroke using CABasicAnimation.
What I want to do is have the stroke of my CAShapeLayer pulse between two colors. Using CABasicAnimation for opacity works fine, but the [CABasicAnimation animationWithKeyPath:#"strokeColor"] eludes me, and I'd appreciate any advice on how to successfully implement.
CABasicAnimation *strokeAnim = [CABasicAnimation animationWithKeyPath:#"strokeColor"];
strokeAnim.fromValue = (id) [UIColor blackColor].CGColor;
strokeAnim.toValue = (id) [UIColor purpleColor].CGColor;
strokeAnim.duration = 1.0;
strokeAnim.repeatCount = 0.0;
strokeAnim.autoreverses = NO;
[shapeLayer addAnimation:strokeAnim forKey:#"animateStrokeColor"];
// CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
// opacityAnimation.fromValue = [NSNumber numberWithFloat:0.0];
// opacityAnimation.toValue = [NSNumber numberWithFloat:1.0];
// opacityAnimation.duration = 1.0;
// opacityAnimation.repeatCount = 0.0;
// opacityAnimation.autoreverses = NO;
// [shapeLayer addAnimation:opacityAnimation forKey:#"animateOpacity"];
Uncommenting the opacity animation results in an expected opacity fade. The stroke animation produces no effect. An implicit strokeColor change animates as expected, but I would like documented confirmation that strokeColor can be explicitly animated using CABasicAnimation.
Update: The specific problem was that shapeLayer.path was NULL. Correcting that fixed the problem.
The code below works great for me. What is the lineWidth of your shapeLayer stroke path? Could that be the issue?
- (void)viewDidLoad
{
[super viewDidLoad];
UIBezierPath * circle = [UIBezierPath bezierPathWithOvalInRect:self.view.bounds];
CAShapeLayer * shapeLayer = [CAShapeLayer layer];
shapeLayer.path = circle.CGPath;
shapeLayer.strokeColor =[UIColor blueColor].CGColor;
[shapeLayer setLineWidth:15.0];
[self.view.layer addSublayer:shapeLayer];
CABasicAnimation *strokeAnim = [CABasicAnimation animationWithKeyPath:#"strokeColor"];
strokeAnim.fromValue = (id) [UIColor redColor].CGColor;
strokeAnim.toValue = (id) [UIColor greenColor].CGColor;
strokeAnim.duration = 3.0;
strokeAnim.repeatCount = 0;
strokeAnim.autoreverses = YES;
[shapeLayer addAnimation:strokeAnim forKey:#"animateStrokeColor"];
}
Let me know if it works for you...