I'm trying to create multiple "cards" to animate them afterwards using this code:
CAReplicatorLayer *cardsWrapperLayer = [CAReplicatorLayer layer];
cardsWrapperLayer.instanceCount = 4;
cardsWrapperLayer.instanceDelay = 10;
cardsWrapperLayer.instanceTransform = CATransform3DMakeTranslation(0, phoneSize.height + self.phonePadding, 0);
[cardsWrapperLayer addSublayer:self.cardLayer];
but they are appearing all at the same time, even if the instanceDelay is set to 10. I have this piece of code in the viewDidAppear method.
instanceDelay doesn’t do anything by itself, it just shifts the “current time” for each instance. To see something happen, you need to add an animation, like this:
CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeIn.fromValue = #0; // if we don’t specify a toValue, it’ll animate to the layer’s current value which by default is 1
fadeIn.duration = 0.2;
fadeIn.removedOnCompletion = NO;
[self.cardLayer addAnimation:appear forKey:#"appear"];
Note that the removedOnCompletion is important—if you let the animation remove itself automatically, then it’ll be gone as soon as the first instance finishes animating and the other instances will snap to their final state. You should remove the animation manually at a later time, like when you know it’ll be over (i.e. the animation’s duration ✕ the replicator layer’s instanceCount)—just call -removeAnimationForKey: on the base layer with the key you added the animation with.
You also have to make sure that you set the animation on the replicator layer's sublayer, not on the replicator layer itself! Bit me hard after 3 years of not working with CAReplicatorLayer again.
Related
I am building a game with SceneKit. It is based on SCNBoxes which get added and removed. At the end there are like 30 boxes added to the root node.
// Prepare Surface Layer for Blocks
self.colorOfBlock = [CALayer layer];
self.colorOfBlock.frame = CGRectMake(0, 0, 1000, 1000);
self.colorOfBlock.backgroundColor = [UIColor colorWithHue:0 saturation:1 brightness:1 alpha:1].CGColor;
self.textLayer = [CATextLayer layer];
self.textLayer.frame = self.colorOfBlock.bounds;
self.textLayer.fontSize = 750;//self.colorOfBlock.bounds.size.height;
self.textLayer.string = #"2";
self.textLayer.alignmentMode = kCAAlignmentCenter;
self.textLayer.foregroundColor = [UIColor blackColor].CGColor;
[self.textLayer display];
[self.colorOfBlock addSublayer:self.textLayer];
// Prepare Material for the Block
SCNMaterial *material = [SCNMaterial material];
material.diffuse.contents = self.colorOfBlock;
// Create the Block
self.blockNode = [SCNNode node];
self.blockNode.geometry = [SCNBox boxWithWidth:0.95 height:0.95 length:0.95 chamferRadius:0.1];
self.blockNode.geometry.firstMaterial = material;
self.blockNode.position = position;
// Setup the physics body
self.blockNode.physicsBody = [SCNPhysicsBody dynamicBody];
self.blockNode.physicsBody.affectedByGravity = NO;
self.blockNode.physicsBody.categoryBitMask = CollisionCategoryBlock;
self.blockNode.physicsBody.contactTestBitMask = CollisionCategoryBorder | CollisionCategoryBlock;
self.blockNode.physicsBody.collisionBitMask = CollisionCategoryBorder | CollisionCategoryBlock;
self.blockNode.physicsBody.angularVelocityFactor = SCNVector3Make(0, 0, 0);
[self addChildNode:self.blockNode];
self.gameValue = 2;
if (position.z < 1) {
self.name = #"Front";
} else {
self.name = #"Back";
}
After a while the textLayer seem to disappear. If new blocks are added, they have no textLayer anymore. And another side effect is that old textLayers on the existing blocks are not updated anymore.
On the next step newly added blocks become invisible. Or better to say - they never get a CALayer. But the physicsBodies are still working.
And sometimes the view crashes. The debugger says something like the position of the crashing area can not be found. After this the view is deformed. But I still can call SCNActions that clears the view and shows up all created Objects that are NOT these blocks like I posted. These blocks are totally kicked out of the view.
By the way when I setup the "phonglighttype" for those blocks this bugging cascade happens earlier.
There is also one more thing I noticed. When I tab out of the App (tested from iPhone 5) and tabbed in again after a while, the
self.gameView.showsStatistics = YES;
shows on the GPU bar a blue line. After 2 or 3 sec, the blue bar is gone and a full green GPU bar shows that it is ready for getting used. And everything works like it should do. After a while these blocks become invisible again. If I tab out and tab in again, newly added Blocks are visible again.
Somehow I think of that I am spamming my device cache until the App crashes. But there is no useful hint from Xcode. I wonder if an option to clear the viewControllers cache might solve this problem - if there is an option for this.
Kind Regards
I did not solved it completely, but I decided to replace every part of CALayer and CATextLayer with a common
material.diffuse.contents = [arrayOfTextures objectAtIndex:certainImage];
With a higher resolution (like Iphone 6 has) it did show up that the CALayer was causing some kind of graphical faulting. But the simple pre-worked image (from an imageArray) solution improves the performance pretty much and prevents from crashing.
I can not surely say if there is no way to solve it with the CALayer approach. But loading images as Material and updating them brought the most satisfying result.
I am using CABasicAnimation to move and resize an image view. I want the image view to be added to the superview, animate, and then be removed from the superview.
In order to achieve that I am listening for delegate call of my CAAnimationGroup, and as soon as it gets called I remove the image view from the superview.
The problem is that sometimes the image blinks in the initial location before being removed from the superview. What's the best way to avoid this behavior?
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil];
animGroup.duration = .5;
animGroup.delegate = self;
[imageView.layer addAnimation:animGroup forKey:nil];
When you add an animation to a layer, the animation does not change the layer's properties. Instead, the system creates a copy of the layer. The original layer is called the model layer, and the duplicate is called the presentation layer. The presentation layer's properties change as the animation progresses, but the model layer's properties stay unchanged.
When you remove the animation, the system destroys the presentation layer, leaving only the model layer, and the model layer's properties then control how the layer is drawn. So if the model layer's properties don't match the final animated values of the presentation layer's properties, the layer will instantly reset to its appearance before the animation.
To fix this, you need to set the model layer's properties to the final values of the animation, and then add the animation to the layer. You want to do it in this order because changing a layer property can add an implicit animation for the property, which would conflict with the animation you want to explicitly add. You want to make sure your explicit animation overrides the implicit animation.
So how do you do all this? The basic recipe looks like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:myLayer.position];
layer.position = newPosition; // HERE I UPDATE THE MODEL LAYER'S PROPERTY
animation.toValue = [NSValue valueWithCGPoint:myLayer.position];
animation.duration = .5;
[myLayer addAnimation:animation forKey:animation.keyPath];
I haven't used an animation group so I don't know exactly what you might need to change. I just add each animation separately to the layer.
I also find it easier to use the +[CATransaction setCompletionBlock:] method to set a completion handler for one or several animations, instead of trying to use an animation's delegate. You set the transaction's completion block, then add the animations:
[CATransaction begin]; {
[CATransaction setCompletionBlock:^{
[self.imageView removeFromSuperview];
}];
[self addPositionAnimation];
[self addScaleAnimation];
[self addOpacityAnimation];
} [CATransaction commit];
CAAnimations are removed automatically when complete. There is a property removedOnCompletion that controls this. You should set that to NO.
Additionally, there is something known as the fillMode which controls the animation's behavior before and after its duration. This is a property declared on CAMediaTiming (which CAAnimation conforms to). You should set this to kCAFillModeForwards.
With both of these changes the animation should persist after it's complete. However, I don't know if you need to change these on the group, or on the individual animations within the group, or both.
Heres an example in Swift that may help someone
It's an animation on a gradient layer. It's animating the .locations property.
The critical point as #robMayoff answer explains fully is that:
Surprisingly, when you do a layer animation, you actually set the final value, first, before you start the animation!
The following is a good example because the animation repeats endlessly.
When the animation repeats endlessly, you will see occasionally a "flash" between animations, if you make the classic mistake of "forgetting to set the value before you animate it!"
var previousLocations: [NSNumber] = []
...
func flexTheColors() { // "flex" the color bands randomly
let oldValues = previousTargetLocations
let newValues = randomLocations()
previousTargetLocations = newValues
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
// AND NOW ANIMATE:
CATransaction.begin()
// and by the way, this is how you endlessly animate:
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
self?.animeFlexColorsEndless()
}
let a = CABasicAnimation(keyPath: "locations")
a.isCumulative = false
a.autoreverses = false
a.isRemovedOnCompletion = true
a.repeatCount = 0
a.fromValue = oldValues
a.toValue = newValues
a.duration = (2.0...4.0).random()
theLayer.add(a, forKey: nil)
CATransaction.commit()
}
The following may help clarify something for new programmers. Note that in my code I do this:
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
// AND NOW ANIMATE:
CATransaction.begin()
...set up the animation...
CATransaction.commit()
however in the code example in the other answer, it's like this:
CATransaction.begin()
...set up the animation...
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
CATransaction.commit()
Regarding the position of the line of code where you "set the values, before animating!" ..
It's actually perfectly OK to have that line actually "inside" the begin-commit lines of code. So long as you do it before the .commit().
I only mention this as it may confuse new animators.
I'm trying to create an UIView and change its alpha property to simulate backlight change.
Here is my code
TransparentView = [[UIView alloc] initWithFrame:self.view.bounds];
TransparentView.backgroundColor = [UIColor whiteColor];
self.view = TransparentView;
TransparentView.alpha = 0.2;
float step = 1.0 / ( appDelegate.OnOffTime * 100);
for (float f = 0; f < 1; f=f+step) {
TransparentView.alpha = (CGFloat)(1 - f);
[NSThread sleepForTimeInterval:0.01];
}
Both TransparentView.alpha = 0.2 and TransparentView.alpha = (CGFloat)(1 - f) do change TransparentView.alpha, but only TransparentView.alpha = 0.2 changes real device "brightness'.
What am I doing wrong?
I think the issue is that you are sleeping the thread, and not actually setting up an asynchronous timer. What this means is, you actually are not letting the run loop get around to making your changes on screen, because you are not exiting this loop until it's back up to 1 :-)
EDIT: to clarify:
Any change you make to your UI will not actually get drawn until the run loop "comes back around", which requires your method to return. You likely want an NSTimer instance, configured to call a method on this object which increments the alpha (and checks the alpha against your destination value; invalidating the timer when it reaches it.
You may want to consider changing your code to use the built-in UIView animation support. This will ensure that your view is updated and you aren't sleeping the main thread. If you need to do something special once the animation completes, you can use the completion: version of the method.
TransparentView.alpha = 0.2;
[UIView animateWithDuration: appDelegate.OnOffTime animations:^{
TransparentView.alpha = 0.0;
}];
Alpha and luminance are two different concepts for a view. If you really wanted to change the luminance of the display, you can use iOS 5+'s [[UIScreen mainScreen]brightness];, otherwise your alpha changes need to be against a black background and not using such strange numbers as a cast 1-float. My bet is that your little algorithm is returning a large enough step factor that your view is being told to display alpha values over one (which defaults to one anyhow).
FUTURE VIEWERS:
I have managed to finish this rotation animation and code with description can be found on tho question. NSImage rotation in NSView is not working
Before you proceed please up vote Duncan C 's answer. As I manage to achieve this rotation from his answer.
I have an image like this,
I want to keep rotating this sync icon, On a different thread. Now I tried using Quartz composer and add the animation to QCView but it is has very crazy effect and very slow too.
Question :
How do I rotate this image continuously with very less processing expense?
Effort
I read CoreAnimation, Quartz2D documentation but I failed to find the way to make it work. The only thing I know so far is, I have to use
CALayer
CAImageRef
AffineTransform
NSAnimationContext
Now, I am not expecting code, but an understanding with pseudo code will be great!
Getting an object to rotate more than 180 degrees is actually a little bit tricky. The problem is that you specify a transformation matrix for the ending rotation, and the system decides to rotate in the other direction.
What I've done is to create a CABasicAnimation of less than 180 degrees, set up to be additive , and with a repeat count. Each step in the animation animates the object more.
The following code is taken from an iOS application, but the technique is identical in Mac OS.
CABasicAnimation* rotate = [CABasicAnimation animationWithKeyPath: #"transform.rotation.z"];
rotate.removedOnCompletion = FALSE;
rotate.fillMode = kCAFillModeForwards;
//Do a series of 5 quarter turns for a total of a 1.25 turns
//(2PI is a full turn, so pi/2 is a quarter turn)
[rotate setToValue: [NSNumber numberWithFloat: -M_PI / 2]];
rotate.repeatCount = 11;
rotate.duration = duration/2;
rotate.beginTime = start;
rotate.cumulative = TRUE;
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
CAAnimation objects operate on layers, so for Mac OS, you'll need to set the "wants layer" property in interface builder, and then add the animation to your view's layer.
To make your view rotate forever, you'd set repeat count to some very large number like 1e100.
Once you've created your animation, you'd add it to your view's layer with code something like this:
[myView.layer addAnimation: rotate forKey: #"rotateAnimation"];
That's about all there is to it.
Update:
I've recently learned of another way to handle rotations of greater than 180 degrees, or continuous rotations.
There is a special object called a CAValueFunction that lets you apply a change to your layer's transform using an arbitrary value, including values that specify multiple full rotations.
You create a CABasicAnimation of your layer's transform property, but then instead of providing a transform, the value you supply is an NSNumber that gives the new rotation angle. If you provide a new angle like 20pi, your layer will rotate 10 full rotations (2pi/rotation). The code looks like this:
//Create a CABasicAnimation object to manage our rotation.
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform"];
rotation.duration = 10.0;
CGFLOAT angle = 20*M_PI;
//Set the ending value of the rotation to the new angle.
rotation.toValue = #(angle);
//Have the rotation use linear timing.
rotation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
/*
This is the magic bit. We add a CAValueFunction that tells the CAAnimation we are
modifying the transform's rotation around the Z axis.
Without this, we would supply a transform as the fromValue and toValue, and
for rotations > a half-turn, we could not control the rotation direction.
By using a value function, we can specify arbitrary rotation amounts and
directions and even rotations greater than 360 degrees.
*/
rotation.valueFunction =
[CAValueFunction functionWithName: kCAValueFunctionRotateZ];
/*
Set the layer's transform to it's final state before submitting the animation, so
it is in it's final state once the animation completes.
*/
imageViewToAnimate.layer.transform =
CATransform3DRotate(imageViewToAnimate.layer.transform, angle, 0, 0, 1.0);
[imageViewToAnimate.layer addAnimation:rotation forKey:#"transform.rotation.z"];
(I Extracted the code above from a working example application, and took out some things that weren't directly related to the subject. You can see this code in use in the project KeyframeViewAnimations (link) on github. The code that does the rotation is in a method called `handleRotate'
This question has been asked before but in a slightly different way and I was unable to get any of the answers to work the way I wanted, so I am hoping somebody with great Core Animation skills can help me out.
I have a set of cards on a table. As the user swipes up or down the set of cards move up and down the table. There are 4 cards visible on the screen at any given time, but only the second card is showing its face. As the user swipes the second card flips back onto its face and the next card (depending on the swipe direction) lands in it's place showing its face.
I have set up my card view class like this:
#interface WLCard : UIView {
UIView *_frontView;
UIView *_backView;
BOOL flipped;
}
And I have tried flipping the card using this piece of code:
- (void) flipCard {
[self.flipTimer invalidate];
if (flipped){
return;
}
id animationsBlock = ^{
self.backView.alpha = 1.0f;
self.frontView.alpha = 0.0f;
[self bringSubviewToFront:self.frontView];
flipped = YES;
CALayer *layer = self.layer;
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / 500;
rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, M_PI, 1.0f, 0.0f, 0.0f);
layer.transform = rotationAndPerspectiveTransform;
};
[UIView animateWithDuration:0.25
delay:0.0
options: UIViewAnimationCurveEaseInOut
animations:animationsBlock
completion:nil];
}
This code works but it has the following problems with it that I can't seem to figure out:
Only half of the card across the x-axis is animated.
Once flipped, the face of the card is upside down and mirrored.
Once I've flipped the card I cannot get the animation to ever run again. In other words, I can run the animation block as many times as I want, but only the first time will animate. The subsequent times I try to animate lead to just a fade in and out between the subviews.
Also, bear in mind that I need to be able to interact with the face of the card. i.e. it has buttons on it.
If anybody has run into these issues it would be great to see your solutions. Even better would be to add a perspective transform to the animation to give it that extra bit of realism.
This turned out to be way simpler than I thought and I didn't have to use any CoreAnimation libraries to achieve the effect. Thanks to #Aaron Hayman for the clue. I used transitionWithView:duration:options:animations:completion
My implementation inside the container view:
[UIView transitionWithView:self
duration:0.2
options:UIViewAnimationOptionTransitionFlipFromBottom
animations: ^{
[self.backView removeFromSuperview];
[self addSubview:self.frontView];
}
completion:NULL];
The trick was the UIViewAnimationOptionTransitionFlipFromBottom option. Incidentally, Apple has this exact bit of code in their documentation. You can also add other animations to the block like resizing and moving.
Ok, this won't be a complete solution but I'll point out some things that might be helpful. I'm not a Core-Animation guru but I have done a few 3D rotations in my program.
First, there is no 'back' to a view. So if you rotate something by M_PI (180 degrees) you're going to be looking at that view as though from the back (which is why it's upside down/mirrored).
I'm not sure what you mean by:
Only half of the card across the x-axis is animated.
But, it it might help to consider your anchor point (the point at which the rotation occurs). It's usually in the center, but often you need it to be otherwise. Note that anchor points are expressed as a proportion (percentage / 100)...so the values are 0 - 1.0f. You only need to set it once (unless you need it to change). Here's how you access the anchor point:
layer.anchorPoint = CGPointMake(0.5f, 0.5f) //This is center
The reason the animation only ever runs once is because transforms are absolute, not cumulative. Consider that you're always starting with the identity transform and then modifying that, and it'll make sense...but basically, no animation occurs because there's nothing to animate the second time (the view is already in the state you're requesting it to be in).
If you're animating from one view to another (and you can't use [UIView transitionWithView:duration:options:animations:completion:];) you'l have to use a two-stage animation. In the first stage of the animation, for the 'card' that is being flipped to backside, you'll rotate the view-to-disappear 'up/down/whatever' to M_PI_2 (at which point it will be 'gone', or not visible, because of it's rotation). And in the second stage, you're rotate the backside-of-view-to-disappear to 0 (which should be the identity transform...aka, the view's normal state). In addition, you'll have to do the exact opposite for the 'card' that is appearing (to frontside). You can do this by implementing another [UIView animateWithDuration:...] in the completion block of the first one. I'll warn you though, doing this can get a little bit complicated. Especially since you're wanting views to have a 'backside', which will basically require animating 4 views (the view-to-disappear, the view-to-appear, backside-of-view-to-disappear, and the backside-of-view-to-appear). Finally, in the completion block of the second animation you can do some cleanup (reset view that are rotated and make their alpha 0.0f, etc...).
I know this is complicated, so you might want read some tutorial on Core-Animation.
#Aaron has some good info that you should read.
The simplest solution is to use a CATransformLayer that will allow you to place other CALayer's inside and maintain their 3D hierarchy.
For example to create a "Card" that has a front and back you could do something like this:
CATransformLayer *cardContainer = [CATransformLayer layer];
cardContainer.frame = // some frame;
CALayer *cardFront = [CALayer layer];
cardFront.frame = cardContainer.bounds;
cardFront.zPosition = 2; // Higher than the zPosition of the back of the card
cardFront.contents = (id)[UIImage imageNamed:#"cardFront"].CGImage;
[cardContainer addSublayer:cardFront];
CALayer *cardBack = [CALayer layer];
cardBack.frame = cardContainer.bounds;
cardBack.zPosition = 1;
cardBack.contents = (id)[UIImage imageNamed:#"cardBack"].CGImage; // You may need to mirror this image
[cardContainer addSublayer:cardBack];
With this you can now apply your transform to cardContainer and have a flipping card.
#Paul.s
I followed your approach with card container but when i applt the rotation animation on card container only one half of the first card rotates around itself and finally the whole view appears.Each time one side is missing in the animation
Based on Paul.s this is updated for Swift 3 and will flip a card diagonally:
func createLayers(){
transformationLayer = CATransformLayer(layer: CALayer())
transformationLayer.frame = CGRect(x: 15, y: 100, width: view.frame.width - 30, height: view.frame.width - 30)
let black = CALayer()
black.zPosition = 2
black.frame = transformationLayer.bounds
black.backgroundColor = UIColor.black.cgColor
transformationLayer.addSublayer(black)
let blue = CALayer()
blue.frame = transformationLayer.bounds
blue.zPosition = 1
blue.backgroundColor = UIColor.blue.cgColor
transformationLayer.addSublayer(blue)
let tgr = UITapGestureRecognizer(target: self, action: #selector(recTap))
view.addGestureRecognizer(tgr)
view.layer.addSublayer(transformationLayer)
}
Animate a full 360 but since the layers have different zPositions the different 'sides' of the layers will show
func recTap(){
let animation = CABasicAnimation(keyPath: "transform")
animation.delegate = self
animation.duration = 2.0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.toValue = NSValue(caTransform3D: CATransform3DMakeRotation(CGFloat(Float.pi), 1, -1, 0))
transformationLayer.add(animation, forKey: "arbitrarykey")
}