Animate a CALayer's bounds w/redraws - iphone

I am wondering how one might animate a CALayer's bounds so, on each bounds change, the layer calls drawInContext:. I've tried the 2 following methods on my CALayer subclass:
Setting needsDisplayOnBoundsChange to YES
Returning YES for the + (BOOL)needsDisplayForKey:(NSString*)key for the bounds key
Neither work. CALayer seems determined to use the layer's original contents and simply scale them according to contentsGravity (which, I assume, is for performance.) Is their a workaround for this or am I missing something obvious?
EDIT: And, incidentally, I noticed that my custom CALayer subclass is not calling initWithLayer: to create a presentationLayer - weird.
Thanks in advance,
Sam

You can use the technique outlined here: override CALayer's +needsDisplayForKey: method and it will redraw its content at every step of the animation.

I'm not sure that setting the viewFlags would be effective. The second solution definitely won't work:
The default implementation returns NO. Subclasses should * call super
for properties defined by the superclass. (For example, * do not try
to return YES for properties implemented by CALayer, * doing will
have undefined results.)
You need to set the view's content mode to UIViewContentModeRedraw:
UIViewContentModeRedraw, //redraw on bounds change (calls -setNeedsDisplay)
Check out Apple's documentation on providing content with CALayer's. They recommend using the CALayer's delegate property instead of subclass, which might be a lot easier than what you're trying now.

I don't know if this entirely qualifies as a solution to your question, but was able to get this to work.
I first pre-drew my contents image into a CGImageRef.
I then overrode the -display method of my layer INSTEAD OF -drawInContext:. In it I set contents to the pre-rendered CGImage, and it worked.
Finally, your layer also needs to change the default contentsGravity to something like #"left" to avoid the contents image being drawn scaled.
The problem I was having was that the context getting passed to -drawInContext: was of the starting size of the layer, not the final post-animation size. (You can check this with the CGBitmapContextGetWidth and CGBitmapContextGetHeight methods.)
My methods are still only called once for the entire animation, but setting the layer's contents directly with the -display method allows you to pass an image larger than the visible bounds. The drawInContext: method does not allow this, as you cannot draw outside the bounds of the CGContext context.
For more about the difference between the different layer drawing methods, see http://www.apeth.com/iOSBook/ch16.html

I'm running into the same problem recently. This is what I found out:
There is a trick you can do it, that is animating a shadow copy of bounds like:
var shadowBounds: CGRect {
get { return bounds }
set { bounds = newValue}
}
then override CALayer's +needsDisplayForKey:.
However, this MAY NOT be what you want to do if your drawing depends on bounds. As you have already noticed, core animation simply scales the contents of layer to animate bounds. This is true even if you do the above trick, that is, the contents are scaled even if the bounds changed during animation. The result is your animation of drawings looks inconsistent.
How to resolve it? Since the content is scaled, you can calculate the values of custom variables determining your drawing by reverse-scaling them so that your drawing on the final but scaled content looks the same as the original and unscaled one, then set the fromValues to these values, the toValues to their old values, animate them at the same time with bounds. If the final values are to be changed, set the toValues to these final values. You must animate at least one custom variable so as to causing the redraw.

This is your custom class:
#implementation MyLayer
-(id)init
{
self = [super init];
if (self != nil)
self.actions = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNull null], #"bounds",
nil];
return self;
}
-(void)drawInContext:(CGContextRef)context
{
CGContextSetRGBFillColor(context,
drand48(),
drand48(),
drand48(),
1);
CGContextFillRect(context,
CGContextGetClipBoundingBox(context));
}
+(BOOL)needsDisplayForKey:(NSString*)key
{
if ([key isEqualToString:#"bounds"])
return YES;
return [super needsDisplayForKey:key];
}
#end
These are additions to xcode 4.2 default template:
-(BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
// create and add layer
MyLayer *layer = [MyLayer layer];
[self.window.layer addSublayer:layer];
[self performSelector:#selector(changeBounds:)
withObject:layer];
return YES;
}
-(void)changeBounds:(MyLayer*)layer
{
// change bounds
layer.bounds = CGRectMake(0, 0,
drand48() * CGRectGetWidth(self.window.bounds),
drand48() * CGRectGetHeight(self.window.bounds));
// call "when idle"
[self performSelector:#selector(changeBounds:)
withObject:layer
afterDelay:0];
}
----------------- edited:
Ok... this is not what you asked for :) Sorry :|
----------------- edited(2):
And why would you need something like that? (void)display may be used, but documentation says it is there for setting self.contents...

Related

How to detect when a property of a class is changed

When changing the frame of a UIView, you have two options. The first would be to pass a CGRect as a parameter to the setFrame function of a view. The other would be to set view.frame equal to the CGRect.
1) [view setFrame:frame];
2) view.frame = frame;
When using the setFrame function of a UIView, it is easy for the view to detect that it should change its frame. However, when simply changing the property (view.frame), how does the view detect that its frame changed (because the view's frame updates immediately)?
That's called dot notation which came with Objective-C 2.0. They're mostly use to get and set properties.
Get a property: view.frame. Set a property: view.frame = frame.
But they're just another way or easier way (syntactic sugar) to write: [view frame] and [view setFrame:frame].
That's all they are, they're just translated to plain old Objective-C messages. In fact, since they're just translated to messages, try something like:
view.setNeedsDisplay;
Note that setNeedsDisplay is not actual property, but since it's translated to [view setNeedsDisplay], it will work! By convention, dot notation is only use for getting and setting properties, so I don't suggest using something like view.setNeedsDisplay.
view.frame = frame equals [view setFrame:frame]. Setting a property using dot method is same as using setXXX:XXX. You can put a NSLog in setXXX:, and call obj.XXX = XXX; And check the console to see if the NSLog is printed.
Example:-
view.frame = frame; //If you set frame like this then
-(void)setFrame:(CGRect)frame
{
//this will be call
}

On iOS, why changing the frame of a CALayer didn't cause an animation?

I have some iOS sample code that if I tap on the main view and the tap handler changes the CALayer object's frame property, the layer object will animate to its position and size.
But if I put that animation inside ViewController's viewDidLoad:
UIImage *shipImage = [UIImage imageNamed:#"SpaceShip1.png"];
ship1 = [[CALayer alloc] init];
ship1.contents = (id) [shipImage CGImage];
ship1.frame = CGRectMake(50, 50, 100, 100);
[self.view.layer addSublayer:ship1];
ship1.frame = CGRectMake(0, 0, 300, 200);
then the animation won't happen. It will just use the (0, 0, 300, 200) with no animation. Why is that and how to make it work?
Several things.
First of all, the frame property is not animatable. Instead, you should be animating bounds (to change the size) and position (to move the layer).
To quote the docs:
Note: The frame property is not directly animatable. Instead you
should animate the appropriate combination of the bounds, anchorPoint
and position properties to achieve the desired result.
Second, you have to return to the event loop after adding a layer before changing an animatable property.
When you return and the system visits the event loop, the next time you change an animatable property, it creates an animation transaction to animate that change.
Since you created a layer, set it's frame, and then changed it, all before returning, your code simply adds the layer at it's final frame.
What you should do instead is to set up the layer, install it into it's parent layer, and return. You can use performSelector:withObject:afterDelay: to invoke the animation after the layer has been added:
- (void)viewDidLoad
{
UIImage *shipImage = [UIImage imageNamed:#"SpaceShip1.png"];
ship1 = [[CALayer alloc] init];
ship1.contents = (id) [shipImage CGImage];
ship1.frame = CGRectMake(50, 50, 100, 100);
[self.view.layer addSublayer:ship1];
[self performSelector: #selector(changeLayer)
withObject: nil
afterDelay: 0.0];
}
- (void) changeLayer
{
//Animate changes to bounds and position, not frame.
ship1.bounds = CGRectMake(0, 0, 300, 200);
//Note that position is based on the center of the layer by default, so you'll
//need to adjust your desired position.
ship1.position = CGPointMake(0,0);
}
You have specified no time delay or animation time. So the object will be moved before it is displayed, and you won't see it in the earlier position. You also need to return to the run loop for the display to show any animation.
There are 3 ways to trigger animations on iOS:
1. UIView Animation
You use a UIView.animateWithDuration method or other variant to directly specify that you want an animation. You can specify parameters such as duration, delay, a completion block and others.
2. Core Animation - Implicit Animation (DESIRED APPROACH)
As the name implies, this animation is triggered without an explicit command. In order to work though, you need to set the CALayer delegate, like this:
ship1.delegate = self;
All the parameters have default values until you say otherwise. For modifying these parameters, you need to call CATransaction:
[CATransition setAnimationDuration:0.2];
IMPORTANT: These CATransaction calls must be placed BEFORE the modifications of the layer's properties, because all animations happen asynchronous (in different runtime threads) and it will be too late for you if you put these calls after.
3. Core Animation - Explicit Animation
You create an CAAnimation (there are different CAAnimation subclass to choose from) and attach it to the layer:
CABasicAnimation *a = [CABasicAnimation alloc] init....
[ship1 addAnimation:a];
I hope this clarifies some doubts about the subject.

drawLayer:inContext is not called

I have my very own minimal view class with this:
- (void) awakeFromNib
{
NSLog(#"awakeFromNib!");
[self.layer setDelegate:self];
[self.layer setFrame:CGRectMake(30, 30, 250, 250)];
self.layer.masksToBounds = YES;
self.layer.cornerRadius = 5.0;
self.layer.backgroundColor = [[UIColor redColor] CGColor];
[self setNeedsDisplay];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
NSLog(#"drawing!");
}
drawLayer:inContext never get called, although I can see the layer as red, rounded corner rectangle. What am I missing?
EDIT: from Apple docs
You can draw content for your layer,
or better encapsulate setting the
layer’s content image by creating a
delegate class that implements one of
the following methods: displayLayer:
or drawLayer:inContext:.
Implementing a delegate method to draw
the content does not automatically
cause the layer to draw using that
implementation. Instead, you must
explicitly tell a layer instance to
re-cache the content, either by
sending it a setNeedsDisplay or
setNeedsDisplayInRect: message, or by
setting its needsDisplayOnBoundsChange
property to YES.
Also
drawLayer:inContext:
If defined, called by the default implementation
of drawInContext:.
You should never change the delegate of layer of a UIView. From documentation of UIView layer property:
Warning: Since the view is the layer’s
delegate, you should never set the
view as a delegate of another CALayer
object. Additionally, you should never
change the delegate of this layer.
If you want to do custom drawing in a view simply override the drawRect: method.
If you do want to use layers you need to create your own:
UIView *myView = ...
CALayer *myLayer = [CALayer layer];
myLayer.delegate = self;
[myView.layer addSublayer:myLayer];
In both cases you need to call setNeedsDisplay on the view in the first case and on your custom layer in the second. You never call drawRect: or drawLayer:inContext: directly, they are called automatically when you call setNeedsDisplay.
Use
[self.layer setNeedsDisplay];
instead of
[self setNeedsDisplay];
It is the view.layer, not the view

Gradient layer not showing up/working on an existing View

I have a view with has another view (called swipeView) inside it. I'm trying to gradient color it using the CAGradientLayer - the code is below
CAGradientLayer *layer = [CAGradientLayer layer];
layer.colors = [NSArray arrayWithObjects:(id)[UIColor darkGrayColor].CGColor,
[UIColor lightGrayColor].CGColor, nil];
layer.frame = swipeView.bounds;
[swipeView.layer insertSublayer:layer atIndex:0];
I'm calling this in the ViewDidLoad method of the main view controller. I see no effect - the view I'm trying to color remains just the same. What am I doing wrong? Are there any other settings/ordering etc. I should know about?
I don't know if this is the problem you're having, but I was just having the exact same symptom, and after a few hours of bafflement, I discovered my problem: I was creating a button, adding the gradient layer, and only later positioning the button. Initially, the button dimensions are all 0, and when setting layer.frame = swipeView.bounds; I was setting the gradient layer to have 0 dimensions too. I added code so that when sizing the button, I also sized the sublayers to match, and this fixed the problem. I'm actually using MonoTouch, so the code looks like this:
btn.Frame = new RectangleF([the dimensions you need]);
foreach(var s in btn.Layer.Sublayers)
{
s.Frame = btn.Bounds;
}
Maybe you're creating a view in code, and the view hasn't been sized or positioned, and so when you add the gradient it also has no size, and that's why you can't see it. When the view changes size (in ViewDidLayoutSubviews maybe), resize the gradient layer and see if that solves it.
That should work. Are you sure you hooked up your swipeView outlet correctly? Debug your -viewDidLoad and verify that swipeView is not nil. Also double check that the bounds (layer.frame) is something sensible.
It shouldn't be necessary, but you can try throwing in a [layer setNeedsDisplay] just to force the layer to render again, but I don't think that is your issue here.
CAGradientLayer *layer = [CAGradientLayer layer];
layer.colors = [NSArray arrayWithObjects:(id)[UIColor darkGrayColor].CGColor,
(id)[UIColor lightGrayColor].CGColor, nil];
layer.frame = swipeView.bounds;
[swipeView.layer insertSublayer:layer atIndex:0];
Make sure that swipeView is not nil and try swipeView.frame.
You don't need the nil terminator at the end of the colors array.
Also, I found insertSublayer did not work, with index either 0 or 1. Changing to simply addSublayer did the trick.

iphone, objective c, xcode

I defined a UIView "RowOfThree" inwhich there are 3 labels. i also defined a UIView "Table" inwhich there are numer of objects of type "Row".
the following code is in a method within object "Table":
RowOfThree *rowOfThree = [[RowOfThree alloc] init];
[self addSubview:rowOfThree];
for some reason it doesn't add the view.
i tried defining the labels in "RowOfThree" both in IB and programmatically and it still didn't work.
Thank you.
Typically, a UIView (and the subclasses) are initialized using initWithFrame:. Maybe you did that in your own implementation of init, I don't know, but it may very well be that your view has a frame of {0,0,0,0} and therefore 0 height and 0 width. Set the frame by hand and tell us whether this works.
CGRect newFrame = CGRectMake(0.f, 0.f, 200.f, 40.f);
RowOfThree *rowOfThree = [[RowOfThree alloc] initWithFrame:newFrame];
[self addSubview:rowOfThree];
[rowOfThree release];
SanHalo's answer is most likely correct but in addition, if you're using Interface Builder, you should not be directly initializing views that are defined in the nib. If you do, you have to use initFromNib instead of just init.