NSView CALayer Opacity Madness - objective-c

It seems as though the layer's properties of my NSView are sometimes not editable/wrong. In the code below, the animation works perfectly, and all appears normal. The output from the NSlogs are always :
anim over opacity = 1.00000
first opacity = 0.50000
current opacity = 0.00000
updated opacity = 0.00000
The first two logs look right, so even at animation did stop, the layer seems to operate normally. However, some time later, when I check the opacity it magically turned to 0. Further wrong, when I set the layer's opacity to 1, and check it immediately after, it still is 0. How is that possible?
I goofed around with setneedsdisplay in the layer and setneedsdisplay:YES in nsview and that
didn't help. Any suggestions are greatly appreciated.
- (void) someSetupAnimationMethod {
aLayer = [CALayer layer];
[theView setWantsLayer:YES];
[theView setLayer:aLayer];
[aLayer setOpacity:0.0];
CABasicAnimation *opacity = [CABasicAnimation animationWithKeyPath:#"opacity"];
opacity.byValue = [NSNumber numberWithFloat:1.0];
opacity.duration = 0.3;
opacity.delegate = self;
opacity.fillMode = kCAFillModeForwards;
opacity.removedOnCompletion = NO;
[opacity setValue:#"opacity done" forKey:#"animation"];
[aLayer addAnimation:opacity forKey:nil];
}
- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
if([[anim valueForKey:#"animation"] isEqualToString:#"opacity done"]) {
NSLog(#"anim over opacity = %f", aLayer.opacity);
aLayer.opacity = 0.5;
[aLyaer removeAllAnimations];
NSLog(#"first opacity = %f", aLayer.opacity);
}
}
- (void) someLaterMethod {
NSLog(#"current opacity = %f", aLayer.opacity);
aLayer.opacity = 1.0;
NSLog(#"updated opacity = %f", aLayer.opacity);
}

You're breaking a fundamental CALayer/NSView rule by creating a layer-backed view and then trying to manipulate the layer directly.
aLayer = [CALayer layer];
[theView setWantsLayer:YES];
[theView setLayer:aLayer];
When you tell the view to use a layer before calling setLayer:, the view becomes "layer-backed" -- it is simply using the layer as a buffer and all drawing that you want to do should be done through the usual drawRect: and related methods. You are not allowed to touch the layer directly. If you change the order of those calls:
[theView setLayer:aLayer];
[theView setWantsLayer:YES];
You now have a "layer-hosting" view, which means that the view just provides a space on the screen for the layer to be drawn into. In this case, you do your drawing directly into the layer. Since the contents of the layer become the contents of the view, you cannot add subviews or use the view's drawing mechanisms.
This is mentioned in the -[NSView setWantsLayer:] documentation; I also found an old cocoa-dev thread that explains it pretty well.
I'm not certain that this will fix your problem, but as far as I can see you're doing everything else correctly, such as setting the fill mode and updating the value after the animation finishes.

Related

how to animate both the frame of an UIView and the frame of one of its sublayers?

What I have:
I have a UIView (named pView) which has as sublayer a CAGradientLayer. Practically is this:
ViewController -> View ->pView -> CAGradientLayer
This is the code that creates all this:
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITestView * pView = nil;
UIColor * scolor = nil, *ecolor = nil;
self.view.backgroundColor = [UIColor yellowColor];
pView = [[UITestView alloc] initWithFrame: CGRectMake(self.view.frame.origin.x, 100.0, self.view.frame.size.width, 100.0)];
scolor = [UIColor colorWithRed:(14/255.0) green: (238/255.0) blue:(123/255.0) alpha:1];
ecolor = [UIColor colorWithRed:(6/255.0) green: (216/255.0) blue:(69/255.0) alpha:1];
// creating the gradient layer
CAGradientLayer * layer = [[CAGradientLayer alloc] init];
layer.frame = self.bounds;
layer.colors = #[(id)scolor.CGColor,(id)ecolor.CGColor];
[pView.layer insertSublayer:layer atIndex:0];
// creating a tapGestureRecognizer
[pView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapInViews:)]];
[self.view addSubview:pView];
touched = NO;
}
....
#end
What I'm trying to do
Once a tap gesture is detected over pView I want to increase the height of pView by 100.0 but animatedly. If pView was previously touched (that is, its height was already increased by 100.0) I want to decrease the height of pView by 100.0 (that is, returning it to its original size);
What I already know
I know that since I want to change the frame of pView I must change also the frame (or bounds) of the CAGradientLayer attached to pView. Since I want to animate these changes, I want these animations occurs at the same time and have the same duration.
I know that the frame (or bounds) of a layer is only animatable inside an animation block.
What I do:
This is the option I've test:
- (void) handleTapInViews: (nonnull UITapGestureRecognizer *) sender {
pView = sender.view;
if (touched) {
[UIView animateWithDuration:0.4 animations:^{
pView.frame = CGRectMake(pView.frame.origin.x, pView.frame.origin.y, pView.frame.size.width, pView.frame.size.height - 100.0);
pView.layer.sublayers[0].frame = pView.bounds;
[self.view setNeedsLayout];
} completion:^(BOOL finished) {
touched = NO;
}];
}
else {
[UIView animateWithDuration:0.4 animations:^{
pView.frame = CGRectMake(pView.frame.origin.x, pView.frame.origin.y, pView.frame.size.width, pView.frame.size.height + 100.0);
pView.layer.sublayers[0].frame = pView.bounds;
[self.view setNeedsLayout];
} completion:^(BOOL finished) {
touched = YES;
}];
}
}
This option actually works (that is, both the frame of pView and the frame of the layer change animatedly) but they are not synchronised; that is, the changes in the frame of the layer are slightly (but perceptible) more faster than the changes in the frame of pView. This effect is more evident when the height of pView is decreased.
I 've also read about CABasicAnimation and CAAnimationGroup, in order to animate both the bounds and position of the layer. In this case also the animation of the layer is bit faster than the animation of the view (it is perceptible and it is not a matter of seconds in case anyone ask about setting duration or whatever) and also in this case, after the animation the bound of the layer return to its original size which is not the desired effect. I already know this last matter can be fixed assigned the new values to the layer at the end of the animation but I certainty do not know where in my code put that.
Most of what i've read regarding this other option is from these links:
link1
link2
link3
link4
In any case, does anybody please knows how can I fix this?? thanks in advance.
Well I ended setting the backgroundColor of Pview as ecolor = [UIColor colorWithRed:(6/255.0) green: (216/255.0) blue:(69/255.0) alpha:1. This way, the effect I commented about the animation of the layer been faster than the animation of the view is not noticeable now. I think maybe this is not the proper answer but it suit me.

How to animate a CALayer attached to UIImageView?

I am using this code proposed by Bartosz to add a mask to an UIImageView. It works fine.
#import <QuartzCore/QuartzCore.h>
CALayer *mask = [CALayer layer];
mask.contents = (id)[[UIImage imageNamed:#"mask.png"] CGImage];
mask.frame = CGRectMake(0, 0, 320.0, 100.0);
yourImageView.layer.mask = mask;
yourImageView.layer.masksToBounds = YES;
In addition, I want to animate the mask, e.g. sliding the mask to the right, so that at the end of the animation, the mask is not applied to the UIImageView any more.
In my specific case, the mask uses a fully transparent image, so the UIImageView is not visible at the initial state (which works fine), but is expected to be so at the end of the animation. However, the idea may be reused to any other use case were masks need to be animated.
The idea is to manipulate the x-origin portion of the frame of the mask. So, I came up with this code:
[UIView animateWithDuration: 0.2
delay: 0
options: UIViewAnimationCurveEaseInOut
animations:^{
CGRect maskFrame = yourImageView.layer.mask.frame;
maskFrame.origin.x = 320.0;
yourImageView.layer.mask.frame = maskFrame;
}
completion:^(BOOL finished){}];
Unfortunately, the mask is applied to the whole UIImageView at any time, it's not sliding to the right.
UPDATE 1:
This is the code I am actually using the set up the view and mask: It's a UITableViewCell.
APPCell.m (APPCell.h "extends" UITableViewCell)
#import "APPCell.h"
#import <QuartzCore/QuartzCore.h>
#interface APPCell()
#property (strong, nonatomic) UIImageView *menu;
#property (strong, nonatomic) CALayer *menuMask;
...
#end
#implementation APPCell
...
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self.menu = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 320.0, 88.0)];
[self.menu setBackgroundColor:[UIColor clearColor]];
[self.menu setImage:[UIImage imageNamed:#"cell_back"]];
[self addSubview:self.menu];
self.menuMask = [CALayer layer];
self.menuMask.contents = (id)[[UIImage imageNamed:#"cell_mask"] CGImage];
self.menuMask.frame = CGRectMake(0, 0, 320.0, 88.0);
self.menu.layer.mask = self.menuMask;
self.menu.layer.masksToBounds = YES;
}
...
Instead of animating with the help of UIKit, I am now using implicit animation of CoreAnimation to move the mask layer:
APPCell.m
...
- (void)swipeLeft
{
self.menuMask.position = CGPointMake(-320.0, 0.0);
}
...
I can confirm that swipeLeft is called. I expect the mask "to be gone" and to see the [UIImage imageNamed:#"cell_back"]], which I do when I uncomment self.menu.layer.mask = self.menuMask.
Solution:
Instead of setting the content on the CALayer, I set the background color to white. This is the code I am using:
self.menuSubMenuMask = [CALayer layer];
self.menuSubMenuMask.backgroundColor = [[UIColor whiteColor] CGColor];
self.menuSubMenuMask.frame = CGRectMake(320.0, 0.0, 320.0, 88.0);
self.tableCellSubMenu.layer.mask = self.menuSubMenuMask;
self.tableCellSubMenu.layer.masksToBounds = YES;
In order to show the UIImageView the CALayer is applied to, the CALayer must NOT be "above" the UIImageView.
Animation with UIKit of UIViews is much more limited than using Core Animation directly. In particular what you are trying to animate is not one of animatable properties of a UIView. In addition as clarified in the View Programming Guide for iOS:
Note: If your view hosts custom layer objects—that is, layer objects without an associated view—you must use Core Animation to animate any changes to them.
This is the case in your example. You have added a CALayer to your view and UIKit will not be able to animate the result for you. On the other hand you can use Core Animation directly to animate the motion of your mask layer. You should be able to do this easily using implicit animation as described in the Core Animation Programming Guide. Please note that from the list of CALayer Animatable Properties that frame is not animatable. Instead you should use position.
You can achieve something you want by using CATransition, although this might not be the solution you want:
1) At first, set mask for your layer just as you did
2) When you want to remove mask and reveal your image, use the following code:
CATransition* transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromRight;
transition.duration = 1.0;
[mask addAnimation:transition forKey:kCATransition];
imageView.layer.mask.contents = [UIImage imageNamed:#"black.png"].CGImage;
The main trick here - we created transition animation for our mask layer, so this animation will be applied when you change any (i'm not sure about any) property of mask layer. Now we set mask's content to completely black image to remove masking at all - now we've got smooth pushing animation where our masked image is going to the left and unmasked image is getting into its place
The easiest way is to use CoreAnimation itself:
CGPoint fromPoint = mask.position;
CGPoint toPoint = CGPointMake(fromPoint.x*3.0, fromPoint.y);
mask.position = toPoint; // CoreAnimation animations do *not* persist
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:fromPoint];
animation.toValue = [NSValue valueWithCGPoint:toPoint];
animation.duration = 4.0f;
[mask addAnimation:animation forKey:#"position"];

UIImage content disappearing (and reappearing) when using drawLayer:inContext

I'm using the delegate method drawLayer:inContext to change the background opacity for a small box as it moves around the screen. The layer had a UIImage set as the content when it was initialized.
Every time drawLayer:inContext is called the image will either disappear or reappear (toggling back and forth) making for a stutter-y looking movement on screen.
I have the image data as a local ivar in the delegate class so that it doesn't need to reload every time.
I'm not sure what's causing this. Any ideas? Here's the code I'm using:
- (id)init
{
self = [super init];
if(self) {
UIImage *layerImage = [UIImage imageNamed:#"Hypno.png"];
image = layerImage.CGImage;
}
return self;
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
float alpha = layer.position.y / layer.superlayer.bounds.size.height;
UIColor *reddish = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:alpha];
CGColorRef cgReddish = reddish.CGColor;
layer.backgroundColor = cgReddish;
[layer setContents:(id)image];
}
Don't use -drawLayer:inContext:. That method is for drawing into a CG context which will then become the layer's contents, using Core Graphics or UIKit methods. You don't want to change the layer's contents at all -- you want to set it to be your image, once, and then never touch it again.
Your image is appearing and disappearing, because sometimes the layer displays the image, and other times the layer displays what you drew into the context in -drawLayer:inContext: -- namely, nothing.
(Also: in order for -drawLayer:inContext: to be called, you must be calling [layer setNeedsDisplay], or you have the layer's needsDisplayOnBoundsChange turned on, or something similar. Don't do any of that.)
Instead, try using the CALayer layout machinery. For instance, in your layer's delegate:
- (void)layoutSublayersOfLayer:(CALayer *)layer
{
float alpha = layer.position.y / layer.superlayer.bounds.size.height;
UIColor *reddish = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:alpha];
CGColorRef cgReddish = reddish.CGColor;
layer.backgroundColor = cgReddish;
}
This will get called as part of the display process, anytime the layer or any of its superlayers have had their -setNeedsLayout method called. Often this happens automatically, but depending on exactly what causes the layer to move, you may need to do it manually. Try it and see.
(Alternatively: Since you already have code that changes the layer's position, why not change the layer's background color at the same time?)

Animating a gaussian blur using core animation?

I'm trying to animate something where it's initially blurry then it comes into focus. I guess it works OK, but when the animation is done it's still a little blurry. Am I doing this wrong?
CABasicAnimation* blurAnimation = [CABasicAnimation animation];
CIFilter *blurFilter = [CIFilter filterWithName:#"CIGaussianBlur"];
[blurFilter setDefaults];
[blurFilter setValue:[NSNumber numberWithFloat:0.0] forKey:#"inputRadius"];
[blurFilter setName:#"blur"];
[[self layer] setFilters:[NSArray arrayWithObject:blurFilter]];
blurAnimation.keyPath = #"filters.blur.inputRadius";
blurAnimation.fromValue = [NSNumber numberWithFloat:10.0f];
blurAnimation.toValue = [NSNumber numberWithFloat:1.0];
blurAnimation.duration = 1.2;
[self.layer addAnimation:blurAnimation forKey:#"blurAnimation"];
Your problem is that the animation stops and is automatically removed, but the filter lingers with the tiniest of blur applied.
What you want to do is to remove the blur filter when the animation completes. You need to add a delegate to the CABasicAnimation instance and implement the -[id<CAAnimationDelegate> animationDidStop:finished:] method.
If you let self be the delegate in this case it should be fairly simple, add this line before adding the animation to your layer:
blurAnimation.delegate = self;
And the callback is equally simple:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
[[self layer] setFilters:nil];
}
If you're looking for an optimized way to animate a blur then I recommend creating a single blurred image of your view and then fading the blurred image from alpha 0 to 1 over the top of your original view. Seems nice and fast in tests.

How to add an animated layer at a specific index

I am adding two CAText layers to a view and animating one of them. I want to animate one layer above the other but it doesn't get positioned correctly in the layer hierarchy until the animation has finished. Can anyone see what I have done wrong? The animation works, it is just running behind 'topcharlayer2' until the animation has finished.
- (CABasicAnimation *)topCharFlap
{
CABasicAnimation *flipAnimation;
flipAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
flipAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(1.57f, 1, 0, 0)];
flipAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0.0, 1, 0, 0)];
flipAnimation.autoreverses = NO;
flipAnimation.duration = 0.5f;
flipAnimation.repeatCount = 10;
return flipAnimation;
}
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
[self setBackgroundColor:[UIColor clearColor]]; //makes this view transparent other than what is drawn.
[self initChar];
}
return self;
}
static CATransform3D CATransform3DMakePerspective(CGFloat z)
{
CATransform3D t = CATransform3DIdentity;
t.m34 = - 1. / z;
return t;
}
-(void) initChar
{
UIFont *theFont = [UIFont fontWithName:#"AmericanTypewriter" size:FONT_SIZE];
self.layer.sublayerTransform = CATransform3DMakePerspective(-1000.0f);
topHalfCharLayer2 = [CATextLayer layer];
topHalfCharLayer2.bounds = CGRectMake(0.0f, 0.0f, CHARACTERS_WIDTH, 100.0f);
topHalfCharLayer2.string = #"R";
topHalfCharLayer2.font = theFont.fontName;
topHalfCharLayer2.fontSize = FONT_SIZE;
topHalfCharLayer2.backgroundColor = [UIColor blackColor].CGColor;
topHalfCharLayer2.position = CGPointMake(CGRectGetMidX(self.bounds),CGRectGetMidY(self.bounds));
topHalfCharLayer2.wrapped = NO;
topHalfCharLayer1 = [CATextLayer layer];
topHalfCharLayer1.bounds = CGRectMake(0.0f, 0.0f, CHARACTERS_WIDTH, 100.0f);
topHalfCharLayer1.string = #"T";
topHalfCharLayer1.font = theFont.fontName;
topHalfCharLayer1.fontSize = FONT_SIZE;
topHalfCharLayer1.backgroundColor = [UIColor redColor].CGColor;
topHalfCharLayer1.position = CGPointMake(CGRectGetMidX(self.bounds),CGRectGetMidY(self.bounds));
topHalfCharLayer1.wrapped = NO;
//topHalfCharLayer1.zPosition = 100;
[topHalfCharLayer1 setAnchorPoint:CGPointMake(0.5f,1.0f)];
[[self layer] addSublayer:topHalfCharLayer1 ];
[[self layer] insertSublayer:topHalfCharLayer2 atIndex:0];
[topHalfCharLayer1 addAnimation:[self topCharFlap] forKey:#"anythingILikeApparently"];
}
The View which contains this code is loaded by a view controller in loadView. The initChar method is called in the view's initWithFrame method. The target is iOS4. I'm not using setWantsLayer as I've read that UIView in iOS is automatically layer backed and doesn't require this.
A couple thoughts come to mind:
Try adding the 'R' layer to the layer hierarchy before you start the animation.
Instead of inserting the 'T' layer at index 1, use [[self layer] addSublayer: topHalfCharLayer1]; to add it and then do the insert for the 'R' layer with [[self layer] insertSublayer:topHalfCharLayer2 atIndex:0];
Have you tried to play with the layer zPosition? This determines the visual appearance of the layers. It doesn't actually shift the layer order, but will change the way they display--e.g. which layers is in front of/behind which.
I would also suggest you remove the animation code until you get the layer view order sorted. Once you've done that, the animation should just work.
If you have further issues, let me know in the comments.
Best regards.
From the quartz-dev apple mailing list:
Generally in a 2D case, addSublayer will draw the new layer above the
previous. However, I believe this implementation mechanism is
independent of zPosition and probably just uses something like
painter's algorithm. But the moment you add zPositions and 3D, I don't
think you can solely rely on layer ordering. But I am actually unclear
if Apple guarantees anything in the case where you have not set
zPositions on your layers but have a 3D transform matrix set.
So, it seems I have to set the zPosition explicitly when applying 3D transforms to layers.
/* Insert 'layer' at position 'idx' in the receiver's sublayers array.
* If 'layer' already has a superlayer, it will be removed before being
* inserted. */
open func insertSublayer(_ layer: CALayer, at idx: UInt32)