Gradient effect using CAGradientLayer creates horizontal lines.. (effect not smoother) - ios

I want to give my video player a gradient effect at top and bottom of the view. I am using following way for it, but it does not give a smoother effect, i.e. it creates two white horizontal lines at both ending point of the drawing.
Actually, I have created a subclass of UIView that uses a CAGradientLayer as its layer. I have done it to make layer follow superview's orientation and it works fine. Check this answer for reference
Here is sub class of UIView called GradientView
GradientView.h
#interface GradientView : UIView
#property (nonatomic, strong, readonly) CAGradientLayer *layer;
#end
GradientView.m
#implementation GradientView
#dynamic layer;
+ (Class)layerClass {
return [CAGradientLayer class];
}
#end
Instance:
IBOutlet GradientView *viewControls;
Use it:
-(void)applyGradientEffect
{
UIColor *endColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.90];
UIColor *centerColor = [UIColor clearColor];
viewControls.layer.colors = [NSArray arrayWithObjects:
(id)[endColor CGColor],
(id)[centerColor CGColor],
(id)[centerColor CGColor],
(id)[endColor CGColor],
nil];
viewControls.layer.startPoint = CGPointMake(0.5, 1.0);
viewControls.layer.endPoint = CGPointMake(0.5, 0.0);
}
I have also tried with locations property of CAGradientLayer instead of startPoint and endPoint properties, something like following code snippet. Still it is not smoother enough.
viewControls.layer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:0.2],
nil];
The gradient effect is smoother only if I use only 2 colors from
bottom to top like following code. (It will create gradient effect at
bottom edge only, I need such an effect at both top and bottom)
-(void)applyGradientEffect
{
UIColor *endColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.90];
UIColor *centerColor = [UIColor clearColor];
viewControls.layer.colors = [NSArray arrayWithObjects:
(id)[endColor CGColor],
(id)[centerColor CGColor],
nil];
viewControls.layer.startPoint = CGPointMake(0.5, 1.0);
viewControls.layer.endPoint = CGPointMake(0.5, 0.0);
}
What may be the reason? Is it default behavior in this case? Am I doing anything wrong with drawing?

Related

Button title goes behind gradient layer

I know the fact that if we add the title after assigning the gradient to the button as sublayer, button's title will be in front.
But this case is different.
I am changing gradient for normal state and touched down states of button.
Here is my code :
-(void) buttonClicked:(UIButton*)sender{
sender.backgroundColor = [UIColor clearColor];
// Finding old gradient layer to remove it
for (CALayer *layer in [sender.layer.sublayers copy]){
if ([[layer name] isEqualToString:#"gradientLayer"]) {
[layer removeFromSuperlayer];
}
}
// Applying the new gradient
UIColor *colorTwo = [UIColor colorWithRed:1.00 green:0.81 blue:0.00 alpha:0.95]; // Original
UIColor *colorThree = [[UIColor brownColor] colorWithAlphaComponent:1];
UIColor *colorFour = [[UIColor blackColor] colorWithAlphaComponent:0.9];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = sender.layer.bounds;
gradientLayer.colors = [NSArray arrayWithObjects: (id)[[UIColor whiteColor] colorWithAlphaComponent:0.5].CGColor,(id) colorTwo.CGColor,(id) colorThree.CGColor,(id) colorFour.CGColor, nil];
gradientLayer.locations = [NSArray arrayWithObjects: [NSNumber numberWithFloat:0.0f], [NSNumber numberWithFloat:1.0f], nil];
sender.layer.cornerRadius = 5;
gradientLayer.cornerRadius = sender.layer.cornerRadius;
// Adding gradient
[sender.layer addSublayer:gradientLayer];
gradientLayer.name = #"gradientLayer";
// Adding title after applying the gradient ( BUT ALSO TITLE IS GOING BEHIND THE GRADIENT )
sender.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
sender.titleLabel.numberOfLines = 2;
NSString *str = sender.titleLabel.text;
[sender setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[sender setTitle:str forState:UIControlStateNormal];
}
-(void) touchedDown:(UIButton*)sender{
// Finding old gradient layer to remove it
for (CALayer *layer in [sender.layer.sublayers copy]){
if ([[layer name] isEqualToString:#"gradientLayer"]) {
[layer removeFromSuperlayer];
}
}
// Applying the new gradient
CAGradientLayer *gradientLayer1 = [CAGradientLayer layer];
gradientLayer1.frame = sender.layer.bounds;
gradientLayer1.colors = [NSArray arrayWithObjects: (id)[[UIColor whiteColor] colorWithAlphaComponent:0.5].CGColor, (id)[UIColor colorWithWhite:0.4f alpha:0.2f].CGColor,(id)[UIColor colorWithWhite:0.4f alpha:0.2f].CGColor, nil];
gradientLayer1.locations = [NSArray arrayWithObjects: [NSNumber numberWithFloat:0.0f], [NSNumber numberWithFloat:1.0f], nil];
gradientLayer1.cornerRadius = sender.layer.cornerRadius;
// Adding Gradient
[sender.layer addSublayer:gradientLayer1];
gradientLayer1.name = #"gradientLayer";
sender.backgroundColor = [UIColor blackColor];
[sender setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
}
At first time when i launch the app, title is at front. But when i start clicking it, then the title goes behind the gradient. Kindly help me with this problem. Thanks for the time (:
The problem is this line:
[sender.layer addSublayer:gradientLayer];
That means: add the gradient layer in front of all other sublayers. That includes the layer that displays the title label.
Instead, call insertSublayer:atIndex: (or above: or below:) to position the layer among the other layers where you want it.
However, it would be even better not to mess with the button's layers directly at all. Instead of applying the gradient as a layer, draw the gradient (in code) into an image and use that image as the button's background image.
Moreover, you can configure the background image to change automatically when the user is tapping the button.

Gradient Mask is applied multiple times in reused prototype cell

I have a tableview, with a customer UITableViewCell class - this is created as a prototype cell in Storyboard/Builder.
Because my cell is linked to the Storyboard prototype, I reference it as follows (cellIdentifier matches the ID on the prototype cell):
EventsListTableViewCell *cell = (EventsListTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
As such, the cell is always initialised and ready (I can't use "if (cell == nil{...} ")
This is fine, however I want to add a gradient layer to my cell, which I'm doing within cellForRowAtIndex:
gradientMask = [CAGradientLayer layer];
gradientMask.frame = cell.eventImage.layer.bounds;
gradientMask.startPoint = CGPointMake(0.5, 0.2);
gradientMask.endPoint = CGPointMake(0.5, 1.0);
gradientMask.colors = [NSArray arrayWithObjects:
(id)[[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.0f] CGColor],
(id)[[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:1.0f] CGColor],nil];
[cell.eventImage.layer insertSublayer:gradientMask atIndex:0];
The issue here, is that the gradientMask gets applied for each re-use of the cell, so when I scroll down it gets darker and darker
I realise I need to only apply this gradientMask once, when the cell is first created, however I'm not sure where to call this code, as I never 'init' the cell (this is handled by the storyboard)
I do have a custom class for this cell, but it only contains properties and no methods?
There are various ways of achieving this:
1- Property in UITableViewCell subclass
Create a property in the EventsListTableViewCell class which will hold a reference to the gradientMask:
#interface EventsListTableViewCell : UITableViewCell
#property (weak, nonatomic) CAGradientLayer *gradientMask;
#end
And then in the cellForRowAtIndexPath: method:
if (!cell.gradientMask) {
gradientMask = [CAGradientLayer layer];
gradientMask.frame = cell.eventImage.layer.bounds;
gradientMask.startPoint = CGPointMake(0.5, 0.2);
gradientMask.endPoint = CGPointMake(0.5, 1.0);
gradientMask.colors = [NSArray arrayWithObjects:
(id)[[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.0f] CGColor],
(id)[[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:1.0f] CGColor],nil];
[cell.eventImage.layer insertSublayer:gradientMask atIndex:0];
cell.gradientMask = gradientMask;
}
This will make sure that the gradientMask is initialized only once.
2- name property of CALayer
This way, you don't need to create a property and everything can be handled in the cellForRowAtIndexPath: method itself.
BOOL gradientFound = NO;
for (CALayer *layer in cell.eventImage.layer.sublayers)
{
if ([layer.name isEqualToString:#"gradientLayer"])
{
gradientFound = YES;
break;
}
}
if (!gradientFound)
{
gradientMask = [CAGradientLayer layer];
gradientMask.frame = cell.eventImage.layer.bounds;
gradientMask.name = #"gradientLayer"; //Set the name
gradientMask.startPoint = CGPointMake(0.5, 0.2);
gradientMask.endPoint = CGPointMake(0.5, 1.0);
gradientMask.colors = [NSArray arrayWithObjects:
(id)[[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.0f] CGColor],
(id)[[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:1.0f] CGColor],nil];
[cell.eventImage.layer insertSublayer:gradientMask atIndex:0];
}
3- Declaring the gradienLayer in the UITableViewCell subclass itself
This is the cleanest way as it also isolates the code related to the cell within it's class. You can initialize the cell in the awakeFromNib method.
#implementation EventsListTableViewCell
{
CAGradientLayer *gradientMask;
}
-(void)awakeFromNib
{
gradientMask = [CAGradientLayer layer];
gradientMask.frame = cell.eventImage.layer.bounds;
gradientMask.startPoint = CGPointMake(0.5, 0.2);
gradientMask.endPoint = CGPointMake(0.5, 1.0);
gradientMask.colors = [NSArray arrayWithObjects:
(id)[[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.0f] CGColor],
(id)[[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:1.0f] CGColor],nil];
[self.eventImage.layer insertSublayer:gradientMask atIndex:0];
}
#end
Since you are adding this layer yourself out of storyboard file, this layer will net be deleted during the cellReuse cycle. You have to do it manually.
I usually create a subclass for the cell and override prepareForReuse method, where I remove all the custom views and layers I have added there. This way everything will work smoothly.
And don't forget to add your subviews to cell's contentView property only.

Encapsulate drawRect gradients in CAGradientLayer

There are dozens of excellent examples on here and elsewhere of how to use gradients by
-(void)drawRect:(CGRect)rect
However, I'm new to Quartz and it's likely I'm missing something. When I found this, it seems more intelligent to create a CAGradientLayer and then add it to my view with something like:
-(void)viewWillAppear
{
CAGradientLayer *bgLayer = [BackgroundLayer blueGradient];
bgLayer.frame = self.view.bounds;
[self.view.layer insertSublayer:bgLayer atIndex:0];
}
Where the blueGradient is a separate class method (within BackgroundLayer.m) which creates the gradient, as follows:
+ (CAGradientLayer*) blueGradient {
UIColor *colorOne = [UIColor colorWithRed:(120/255.0) green:(135/255.0) blue:(150/255.0) alpha:1.0];
UIColor *colorTwo = [UIColor colorWithRed:(57/255.0) green:(79/255.0) blue:(96/255.0) alpha:1.0];
NSArray *colors = [NSArray arrayWithObjects:(id)colorOne.CGColor, colorTwo.CGColor, nil];
NSNumber *stopOne = [NSNumber numberWithFloat:0.0];
NSNumber *stopTwo = [NSNumber numberWithFloat:1.0];
NSArray *locations = [NSArray arrayWithObjects:stopOne, stopTwo, nil];
CAGradientLayer *headerLayer = [CAGradientLayer layer];
headerLayer.colors = colors;
headerLayer.locations = locations;
return headerLayer;
}
So here's my question. What if instead of this simple linear gradient, I want to create a radial one? How can I extend blueGradient so that it can handle two dimensional gradients? Or what if I want to add gradients on top of gradients? the drawRect function seems so limiting.
Or maybe that's just the wrong approach... then what's the wiring that I'm missing? How do I add gradients to my view(s) using drawRect? I want to be sure I'm doing it in a modular way so I can add gradient overlays, etc, as additional layers as necessary.
As of iOS 7, CAGradientLayer can only draw a linear gradient. It cannot draw a radial gradient.
You can either use the drawRect: approach, or you can draw your gradient into an image and display the image in a view or layer. You can draw it into a UIImage and display it in a UIImageView, or you can draw it into a CGImage and set it as the contents of a CALayer.

CAGradientLayer for different views

I would like to create a color gradient with CAGradientLayer for multiple views with different sizes. I don't know how to define the frame separately:
UIColor *darkOp = [UIColor colorWithRed:0.2f green:0.2f blue:0.27f alpha:1.0];
UIColor *lightOp = [UIColor colorWithRed:0.36f green:0.35f blue:0.42f alpha:1.0];
// Create the gradient
CAGradientLayer *gradient = [CAGradientLayer layer];
// Set colors
gradient.colors = [NSArray arrayWithObjects:
(id)darkOp.CGColor,
(id)lightOp.CGColor,
nil];
//set radius
gradient.cornerRadius = 5.0;
// Set bounds BUT just for one view size
gradient.frame = self.numberRegionView.bounds; //<-- here I can just define one frame size
// Add the gradient to one view
[self.numberRegionView.layer insertSublayer:gradient atIndex:0];
//but how to add the gradient layer to views with different sizes ???
//[self.graphRegionView.layer insertSublayer:gradient atIndex:0]; ???
//[self.barRegionView.layer insertSublayer:gradient atIndex:0]; ???
Thanks!
-(void)setGradientForView:(UIView*)view
{
static UIColor *darkOp = [UIColor colorWithRed:0.2f green:0.2f blue:0.27f alpha:1.0];
static UIColor *lightOp = [UIColor colorWithRed:0.36f green:0.35f blue:0.42f alpha:1.0];
// Create the gradient
CAGradientLayer *gradient = [CAGradientLayer layer];
// Set colors
gradient.colors = [NSArray arrayWithObjects:
(id)darkOp.CGColor,
(id)lightOp.CGColor,
nil];
//set radius
gradient.cornerRadius = 5.0;
// Set bounds BUT just for one view size
gradient.frame = view.bounds; //<-- here I can just define one frame size
// Add the gradient to one view
[view.layer insertSublayer:gradient atIndex:0];
}
Then use this code for your three views:
[self setGradientForView:self.numberRegionView];
[self setGradientForView:self.barRegionView];
[self setGradientForView:self.numberRegionView];

CAGradientLayer sometimes not rendering

I have a CAGradientLayer drawing in the background on viewDidLoad. It works some of the time, but then sometimes it just doesn't render anything persistently until I restart my computer. I can't figure out why some of the time it would work and then other times it won't. It will work lets say 5 builds in a row and then it will just stop rendering. No errors. Anyone have experience with this?
Background Layer Method:
+ (CAGradientLayer*) morningGradient {
UIColor *mornTop = [UIColor colorWithRed:0.843 green:0.722 blue:0.667 alpha:1.000];
UIColor *mornBottom = [UIColor colorWithRed:0.584 green:0.733 blue:0.945 alpha:1.000];
NSArray *colors = [NSArray arrayWithObjects:(id)mornTop.CGColor, mornBottom.CGColor, nil];
NSNumber *stopOne = [NSNumber numberWithFloat:0.0];
NSNumber *stopTwo = [NSNumber numberWithFloat:0.7];
NSNumber *stopThree = [NSNumber numberWithFloat:1.0];
NSArray *locations = [NSArray arrayWithObjects:stopOne, stopTwo, stopThree, nil];
CAGradientLayer *headerLayer = [CAGradientLayer layer];
headerLayer.colors = colors;
headerLayer.locations = locations;
return headerLayer;
}
Draw Method:
-(void)drawGrad
{
NSLog(#"drawing gradient");
CAGradientLayer *bgLayer = [BackgroundLayer morningGradient];
bgLayer.frame = self.view.bounds;
[self.view.layer insertSublayer:bgLayer atIndex:0];
}
ViewDidLoad:
- (void)viewDidLoad
{
[self drawGrad];
[super viewDidLoad];
}
From the documentation of CGGradientCreateWithColors(colorSpace, colors, locations[]) you can read
The locations array should contain the same number of items as the colors array.
I'm assuming that the same is true for CAGradientLayer but can't find anything in the documentation. I'm making this assumption because it makes sense. How would you really interpret two colors and three locations? What should be the color at the third location?
Change your code so that you pass the same number of colors as locations.
create a custom gradient view with round rect of size 3, you need to add the QuartzCore framework and then follow the code below (.h file and .m file)
#import <UIKit/UIKit.h>
#interface CustomGradientView : UIView
#end
#import "CustomGradientView.h"
#import <QuartzCore/QuartzCore.h>
#implementation CustomGradientView
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor *color1=[UIColor whiteColor];
CGColorRef startColor =color1.CGColor;
UIColor *color2=[UIColor redColor];
CGColorRef endColor = color2.CGColor;
drawLinearGradient(context, rect, startColor, endColor);
CGPathRef p = [[UIBezierPath bezierPathWithRoundedRect:rect
cornerRadius:3] CGPath];
CGContextAddRect(context, rect);
CGContextAddPath(context, p);
CGContextEOClip(context);
CGContextClearRect(context, rect);
}
#end

Resources