CAGradientLayer scrolls with UIScrollView - ios

I'm attempting to add a CAGradientLayer overtop of a UIScrollView to fade the edges into transparency.
The gradient is correct, but the mask I'm adding moves when the UIScrollView scrolls.
I've attempted to commit the CATransaction but it is still not successful.
- (void)layoutSubviews
{
[super layoutSubviews];
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
NSObject * transparent = (NSObject *) [[UIColor colorWithWhite:0 alpha:0] CGColor];
NSObject * opaque = (NSObject *) [[UIColor colorWithWhite:0 alpha:1] CGColor];
CAGradientLayer* hMaskLayer = [CAGradientLayer layer];
hMaskLayer.opacity = .7;
hMaskLayer.colors = [NSArray arrayWithObjects:transparent, opaque, opaque, transparent, nil];
hMaskLayer.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.2],
[NSNumber numberWithFloat:0.8],
[NSNumber numberWithFloat:1.0], nil];
hMaskLayer.startPoint = CGPointMake(0, 0.5);
hMaskLayer.endPoint = CGPointMake(1.0, 0.5);
hMaskLayer.bounds = self.bounds;
hMaskLayer.anchorPoint = CGPointZero;
self.layer.mask = hMaskLayer;
[CATransaction commit];
}

the mask I'm adding moves when the UIScrollView scrolls
Not surprising, because the layer's position is based on the bounds of its superlayer, and the bounds is exactly what changes when scrolling.
Simple solution: put the scroll view in a superview with exactly the same size and mask that superview's layer.

Related

CAShapeLayer rendering issue

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.

iOS "erase" drawing with CAShapeLayer animated

I'm trying to implement a circular indicator that draws itself with animation as values change.
I use CAShapeLayer to draw with animation. So far I got it to draw itself from the beginning each time, it works as intended. Now I want to make it more sleek and draw it forward and "erase" depending on if the new value is greater or lower than previous. I'm not sure how to implement the erasing part. There's a background so I cant just draw on top with white colour. Here's an image to better understand how it looks.
Any ideas how erasing part could be achieved? There's some solutions here on SO on similar problems, but those involve no animation.
I use this code to draw:
- (void)drawCircleAnimatedWithStartAngle:(CGFloat)startAngle
endAngle:(CGFloat)endAngle
color:(UIColor *)color
radius:(int)radius
lineWidth:(int)lineWidth {
CAShapeLayer *circle = [CAShapeLayer layer];
circle.name = #"circleLayer";
circle.path = ([UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius-1 startAngle:startAngle endAngle:endAngle clockwise:YES].CGPath);
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor colorWithRed:19.0/255 green:167.0/255 blue:191.0/255 alpha:1].CGColor;
circle.lineWidth = lineWidth;
// Add to parent layer
for (CALayer *layer in self.circleView.layer.sublayers) {
if ([layer.name isEqualToString:#"circleLayer"]) {
[layer removeFromSuperlayer];
break;
}
}
[self.circleView.layer addSublayer:circle];
circle.zPosition = 1;
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 0.5;
drawAnimation.repeatCount = 1.0; // Animate only once..
// 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.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// Add the animation to the circle
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
I was able to do this in a slightly different way from what you're doing. Instead of drawing an arc of a certain length, I made my path a full circle, but only animate it part way. You will notice that I use the presentation layer's strokeEnd to get the toValue, so that if an animation is in progress when you need to change the value of the final strokeEnd, it will start from where the path is currently drawn. Here is the code in my view controller; outline layer draws the gray circle, similar to what you have in your image. I added 5 buttons to my view (for testing) whose tags are used to change the final stroke value of the animation,
#implementation ViewController {
UIView *circleView;
RDShapeLayer *circle;
}
- (void)viewDidLoad {
[super viewDidLoad];
circleView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
CALayer *outlineLayer = [CALayer new];
outlineLayer.frame = circleView.bounds;
outlineLayer.borderWidth = 2;
outlineLayer.borderColor = [UIColor lightGrayColor].CGColor;
outlineLayer.cornerRadius = circleView.frame.size.width/2.0;
[circleView.layer addSublayer:outlineLayer];
[self.view addSubview:circleView];
circle = [[RDShapeLayer alloc] initWithFrame:circleView.bounds color:[UIColor blueColor].CGColor lineWidth:4];
[circleView.layer addSublayer:circle];
}
-(void)animateToPercentWayAround:(CGFloat) percent {
circle.strokeEnd = percent/100.0;
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 1.0;
drawAnimation.fromValue = #([circle.presentationLayer strokeEnd]);
drawAnimation.toValue = #(percent/100.0);
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
- (IBAction)changeStroke:(UIButton *)sender {
[self animateToPercentWayAround:sender.tag];
}
This is the code for the subclassed CAShapeLayer,
-(instancetype)initWithFrame:(CGRect) frame color:(CGColorRef) color lineWidth:(CGFloat) lineWidth {
if (self = [super init]) {
self.path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(frame, 1, 1)].CGPath;
self.fillColor = [UIColor clearColor].CGColor;
self.strokeColor = color;
self.lineWidth = lineWidth;
self.strokeStart = 0;
self.strokeEnd = 0;
}
return self;
}

Set a gradient Background to ImageView, rotation

I want to set a gradient Background to my ImageView. I found a solution for that here on Stack Overflow. But it doesn't really satisfy my needs.
I want the gradient to be from left to right. With my the code I found I am only able to fill it from top to bottom.
I create the gradient layer with this code:
+ (CAGradientLayer *)colorGradientWithColor:(UIColor *)color
{
UIColor *colorOne = color;
UIColor *colorTwo = [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:0.0f];
NSArray *colors = [NSArray arrayWithObjects:(id)colorOne.CGColor, colorTwo.CGColor, nil];
NSNumber *stopOne = [NSNumber numberWithFloat:0.0];
NSNumber *stopTwo = [NSNumber numberWithFloat:0.9];
NSArray *locations = [NSArray arrayWithObjects:stopOne, stopTwo, nil];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.colors = colors;
gradientLayer.locations = locations;
return gradientLayer;
}
Then I add the layer to my UIImageView:
CAGradientLayer *gradientLayer = [BackgroundGradient colorGradientWithColor:difficultyColor];
gradientLayer.frame = myImageView.bounds;
[myImageView.layer insertSublayer:gradientLayer atIndex:0];
And here is the result:
And here my interface setup:
As you can see, there a two UIImageViews. I want to rotate the layer in the gradient view, so it goes from left to right and not how it is now.
With the following code fragment I am able to rotate the whole ImageView. This only kind of solves my problem, but I don't want that, because it kind of destroys my interface..
myImageView.transform = CGAffineTransformMakeRotation(M_PI*1.5f);
So basically I am searching for a solution to do the gradient from left to right instead from the top to the bottom.
Do you guys have any ideas? I am new to XCode and would appreciate every tip!
This is the result I got from below code. I guess this is what you are trying to achieve
- (CAGradientLayer *)colorGradientWithColor:(UIColor *)color
{
UIColor *colorOne = color;
UIColor *colorTwo = [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:0.0f];
NSArray *colors = [NSArray arrayWithObjects:(id)colorOne.CGColor, colorTwo.CGColor, nil];
NSNumber *stopOne = [NSNumber numberWithFloat:0.0];
NSNumber *stopTwo = [NSNumber numberWithFloat:0.9];
NSArray *locations = [NSArray arrayWithObjects:stopOne, stopTwo, nil];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
[gradientLayer setStartPoint:CGPointMake(0.0, 0.5)];//add these lines
[gradientLayer setEndPoint:CGPointMake(1.0, 0.5)];
gradientLayer.colors = colors;
gradientLayer.locations = locations;
return gradientLayer;
}
Try to rotate your layer before adding it to imageview,using transform.
Use startpoint and endpoint for the gradient layer:
BOOL horizontal = YES;
//(YES for left to right or NO for top to bottom)
gradientLayer.startPoint = horizontal ? CGPointMake(0, 0.5) : CGPointMake(0.5, 0);
gradientLayer.endPoint = horizontal ? CGPointMake(1, 0.5) : CGPointMake(0.5, 1);

Bad performance on rotation when using UIView mask

I have a couple of UITableViews which should fade out at the top. They are placed next to each other in a single UIView. Since all the table views are supposed to fade at the same place, I figured that I should set an alpha mask on the container UIView. This works fine, the scrolling is smooth in all table views and the fade looks nice.
The problem is when the interface orientation changes. It's REALLY choppy. And only when the mask is applied. If I remove the fade, everything is smooth.
This is the code for applying said mask:
// If you want the Table Views to "fade out" at the top, use this function to set the height!
- (void) setFadeHeight:(CGFloat)fadeHeight
{
if (fadeHeight == 0.0) {
self.layer.mask = nil;
return;
}
// Create a gradient layer to use as mask
CAGradientLayer *l = [self createGradientLayerWithHeight:fadeHeight];
l.shouldRasterize = NO;
[self.layer setMask:l];
}
// Creates a transparency gradient. Helper function for the above function
- (CAGradientLayer *) createGradientLayerWithHeight:(CGFloat)gradientHeight
{
CAGradientLayer *mask = [CAGradientLayer layer];
mask.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:1.0],
nil];
mask.colors = [NSArray arrayWithObjects:
(__bridge id)[UIColor clearColor].CGColor,
(__bridge id)[UIColor whiteColor].CGColor,
nil];
mask.frame = CGRectMake(0.0, 0.0, self.bounds.size.width, self.bounds.size.height);
// vertical direction
mask.startPoint = CGPointMake(0.0, 0.0);
mask.endPoint = CGPointMake(0.0, gradientHeight / self.bounds.size.height);
return mask;
}
The issue might be related to this Stack Overflow question, but since I have smooth scrolling in my tables I'm not so sure. I've also tried setting shouldRasterize to NO according to the answers in that question.

CABasicAnimation of layer is choppy

I've built a custom progress bar in a UIView object using 3 layers:
a layer that represents the "empty" progress bar and that I use as a background.
a layer that I use to measure progress that appears on top of layer 1.
a layer that simply draws some hashmarks on top of the progress bar.
Layer 2 is animated to stretch the bounds width from 0 to 320 evenly over a period of time, usually about 2 minutes. It works fine but is not as smooth as I would have expected.
Any thoughts on how I could improve the performance or use other/additional animation techniques to make it appear to move more evenly?
The code that creates layer 2 is:
- (void)drawForegroundLayer {
CGColorRef startColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 blue:230.0/255.0 alpha:1.0].CGColor;
CGColorRef endColor = [UIColor colorWithRed:239.0/255.0 green:125.0/255.0 blue:0.0 alpha:1.0].CGColor;
CAGradientLayer *foregroundLayer = [CAGradientLayer layer];
foregroundLayer.frame = CGRectMake(0, 2, 0, 13);
foregroundLayer.anchorPoint = CGPointMake(0.0,0.5);
foregroundLayer.colors = [NSArray arrayWithObjects:(id)startColor, (id)endColor, nil];
foregroundLayer.borderWidth = 0;
foregroundLayer.edgeAntialiasingMask = kCALayerRightEdge;
[self.layer addSublayer:foregroundLayer];
}
And the code that animates layer 2 is:
- (void)start {
CALayer *bar = [[self.layer sublayers] objectAtIndex:1];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"bounds.size.width"];
animation.toValue = [NSNumber numberWithFloat:320.0];
animation.duration = MAXIMUM_TIME_PER_SESSION;
[bar addAnimation:animation forKey:nil];
}

Resources