I can animate a CALayer object by changing a property using CABasicAnimation as with this code:
let animationOfPropertyOpacity = CABasicAnimation(keyPath: "opacity")
animationOfPropertyOpacity.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animationOfPropertyOpacity.fromValue = 0
animationOfPropertyOpacity.toValue = 1
myLayer.addAnimation(animationOfPropertyOpacity, forKey: "fade")
In this case the animation will create a linear interpolation between 0 to 1. I can do the same with other properties like scale, position or rotation.
What If I want to move from point A (0,3) to point B (3,0) but making a circle respect to point C (0,0)?
I want to keep using fromValue to toValue, but animating a custom property that I've made called rotation. Rotation will calculate each interpolation value based on point C as center and making a radius from that center.
Where do I need to put the function that calculates each step?
I've tried subclassing CALayer and adding a custom property called rotation. Then adding code to needsDisplayForKey to redraw the layer:
#NSManaged var rotation: CGFloat
override class func needsDisplayForKey(key: String) -> Bool {
if (key == "rotation") {
return true
}
return super.needsDisplayForKey(key)
}
This calls drawInContext and makes the whole CALayer black. I have a print in drawInContext and it doesn't get called when changing other properties like opacity, it seems it's not necessary to redraw the layer.
There should be a method that is being called each frame of the animation in CALayer where I could implement a function before applying the changes.
I'm also unable to find the right place to implement the functionality of the custom property that I've created to actually reflect the change even without animation. Let's say a property customOpacity that just inverts the effects of opacity and updating the layer accordingly. I only could make that through a function updateRotation(rotation: CGFloat).
Related
Say you have a simple animation
let e : CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
e.duration = 2.0
e.fromValue = 0
e.toValue = 1.0
e.repeatCount = .greatestFiniteMagnitude
self.someLayer.add(e, forKey: nil)
of a layer
private lazy var someLayer: CAShapeLayer
It's quite easy to read the value of strokeEnd each animation step.
Just change to a custom layer
private lazy var someLayer: CrazyLayer
and then
class CrazyLayer: CAShapeLayer {
override func draw(in ctx: CGContext) {
print("Yo \(presentation()!.strokeEnd)")
super.draw(in: ctx)
}
}
It would be very convenient to actually be able to set the property values of the layer at each step.
Example:
class CrazyLayer: CAShapeLayer {
override func draw(in ctx: CGContext) {
print("Yo \(presentation()!.strokeEnd)")
strokeStart = presentation()!.strokeEnd - 0.3
super.draw(in: ctx)
}
}
Of course you can't do that -
attempting to modify read-only layer
Perhaps you could have a custom animatable property
class LoopyLayer: CAShapeLayer {
#NSManaged var trick: CGFloat
override class func needsDisplay(forKey key: String) -> Bool {
if key == "trick" { return true }
return super.needsDisplay(forKey: key)
}
... somehow for each frame
strokeEnd = trick * something
backgroundColor = alpha: trick
}
How can I "manually" set the values of the animatable properties, each frame?
Is there perhaps a way to simply supply (override?) a function which calculates the value each frame? How to set values each frame, on some of the properties, from a calculation of other properties or perhaps another custom property?
(Footnote 1 - a hacky workaround is to abuse a keyframe animation, but that's not really the point here.)
(Footnote 2 - of course, it's often better to just simply animate in the ordinary old manual way with CADisplayLink, and not bother about CAAnimation, but this QA is about CAAnimation.)
Roughly speaking, CoreAnimation gives you the ability to perform the interpolation between certain values according to a certain timing function.
CAAnimation has a private method that applies its interpolated value to the layer at a given time.
The injection of that value into the presentation layer happens after the presentation layer has been created out of the instance of the model layer's class (init(layer:) call), within the stack of display method call before the draw(in ctx: CGContext) method call.
So, assuming that presentation layer's class is the same as your custom layer's class, one way around that is to have the animated property different from the property that is being used for rendering of the layer's contents. You could animate interpolatedValue property, and have another dynamic property strokeEnd that will on the fly calculate appropriate value on every frame out of the interpolatedValue value.
In the edge case, interpolatedValue could be just the animation progress from 0 to 1, and based on that you can dynamically calculate your strokeEnd value within your presentation layer. You can even ask your delegate about the value for a given progress:
#objc #NSManaged open dynamic var interpolatedValue: CGFloat
open override class func needsDisplay(forKey key: String) -> Bool {
var result = super.needsDisplay(forKey: key)
result = result || key == "interpolatedValue"
return result
}
open var strokeEnd : CGFloat {
get {
return self.delegate.crazyLayer(self, strokeEndFor: self.interpolatedValue)
}
set {
...
}
}
let animation = CABasicAnimation(keyPath: "interpolatedValue")
animation.duration = 2.0
animation.fromValue = 0.0
animation.toValue = 1.0
crazyLayer.add(animation, forKey: "interpolatedValue")
Another way around that could be to use CAKeyframeAnimation where you can specify which value has to be assigned to the animatable property at a corresponding time.
In case you might want to assign on each frame a complex value out of interpolated components, I have a project where I do that for animation of NSAttributedString value by interpolation of its attributes, and the article where I describe the approach.
I'd like to animate a drawing sequence. My code draws a spiral into a UIImageView.image. The sequence changes the image contents, but also changes the scale of the surrounding UIImageView. The code is parameterized for the number of revolutions of the spiral:
func drawSpiral(rotations:Double) {
let scale = scaleFactor(rotations) // do some math to figure the best scale
UIGraphicsBeginImageContextWithOptions(mainImageView.bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.scaleBy(x: scale, y: scale) // some animation prohibits changes!
// ... drawing happens here
myUIImageView.image = UIGraphicsGetImageFromCurrentImageContext()
}
For example, I'd like to animate from drawSpiral(2.0) to drawSpiral(2.75) in 20 increments, over a duration of 1.0 seconds.
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values? How? Is there a better animation approach?
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values
Animation is merely a succession of timed intermediate values being thrown at something. It is perfectly reasonable to ask that they be thrown at your code so that you can do whatever you like with them. Here's how.
You'll need a special layer:
class MyLayer : CALayer {
#objc var spirality : CGFloat = 0
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(spirality) {
return true
}
return super.needsDisplay(forKey:key)
}
override func draw(in con: CGContext) {
print(self.spirality) // in real life, this is our signal to draw!
}
}
The layer must actually be in the interface, though it can be impossible for the user to see:
let lay = MyLayer()
lay.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
self.view.layer.addSublayer(lay)
Subsequently, we can initialize the spirality of the layer:
lay.spirality = 2.0
lay.setNeedsDisplay() // prints: 2.0
Now when we want to "animate" the spirality, this is what we do:
let ba = CABasicAnimation(keyPath:#keyPath(MyLayer.spirality))
ba.fromValue = lay.spirality
ba.toValue = 2.75
ba.duration = 1
lay.add(ba, forKey:nil)
CATransaction.setDisableActions(true)
lay.spirality = 2.75
The console shows the arrival of a succession of intermediate values over the course of 1 second!
2.03143266495317
2.04482554644346
2.05783333256841
2.0708108600229
2.08361491002142
2.0966724678874
2.10976020619273
2.12260236591101
2.13551922515035
2.14842618256807
2.16123360767961
2.17421661689878
2.18713565543294
2.200748950243
2.21360073238611
2.2268518730998
2.23987507075071
2.25273013859987
2.26560932397842
2.27846492826939
2.29135236144066
2.30436328798532
2.31764804571867
2.33049770444632
2.34330793470144
2.35606706887484
2.36881992220879
2.38163591921329
2.39440815150738
2.40716737508774
2.42003352940083
2.43287514150143
2.44590276479721
2.45875595510006
2.47169743478298
2.48451870679855
2.49806520342827
2.51120449602604
2.52407149970531
2.53691896796227
2.54965999722481
2.56257836520672
2.57552136480808
2.58910304307938
2.60209316015244
2.6151298135519
2.62802086770535
2.64094598591328
2.6540260463953
2.6669240295887
2.6798157542944
2.69264766573906
2.70616912841797
2.71896715462208
2.73285858333111
2.74564798176289
2.75
2.75
2.75
Those are exactly the numbers that would be thrown at an animatable property, such as when you change a view's frame origin x from 2 to 2.75 in a 1-second duration animation. But now the numbers are coming to you as numbers, and so you can now do anything you like with that series of numbers. If you want to call your method with each new value as it arrives, go right ahead.
Personally, in more complicated animations I would use lottie the animation itself is built with Adobe After Effect and exported as a JSON file which you will manage using the lottie library this approach will save you time and effort when you port your app to another platform like Android as they also have an Android Lottie which means the complicated process of creating the animation is only done once.
Lottie Files has some examples animations as well for you to look.
#Matt provided the answer and gets the checkmark. I'll recap some points for emphasis:
UIView animation is great for commonly animated properties, but if
you need to vary a property not on UIView's animatable list, you can't use it. You must
create a new CALayer and add a CABasicAnimation(keyPath:) to it.
I tried but was unable to get my CABasicAnimations to fire by adding them to the default UIView.layer. I needed to create a custom CALayer
sublayer to the UIView.layer - something like
myView.layer.addSublayer(myLayer)
Leave the custom sublayer installed and re-add the CABasicAnimation to that sublayer when (and only when) you want to animate drawing.
In the custom CALayer object, be sure to override class func needsDisplay(forKey key: String) -> Bool with your key property (as #Matt's example shows), and also override func draw(in cxt: CGContext) to do your drawing. Be sure to decorate your key property with #objc. And reference the key property within the drawing code.
A "gotcha" to avoid: in the UIView object, be sure to null out the usual draw method (override func draw(_ rect: CGRect) { }) to avoid conflict between animated and non-animated drawing on the separate layers. For coordinating animated and non-animated content in the same UIView, it's good (necessary?) to do all your drawing from your custom layer.
When doing that, use myLayer.setNeedsDisplay() to update the non-animated content within the custom layer; use myLayer.add(myBasicAnimation, forKey:nil) to trigger animated drawing within the custom layer.
As I said above, #Matt answered - but these items seemed worth emphasizing.
I wondering why when I try to animate the path property of a CAShapeLayerwith a basic animation it works but when I try to do it with transaction it doesn't.
I've successfully animated other animatable properties using just transaction. Here is my current code:
CATransaction.begin()
CATransaction.setAnimationDuration(2.0)
path = scalePath() // a scaled version of the original path
CATransaction.commit()
the new path is obtained scaling the original path with this (very hardcoded) function inside an extension of CAShapeLayer:
func scalePath()->CGPath{
var scaleTransform = CGAffineTransform.identity.translatedBy(x: -150, y: -150)
scaleTransform = scaleTransform.scaledBy(x: 10, y: 10)
let newPath = path?.copy(using: &scaleTransform)
return newPath!
}
Can you identify any issue?
The answer is simple but a bit unsatisfactory: while the path property is animatable, it doesn't support implicit animations. This is called out in the Discussion section of the documentation for the path property:
Unlike most animatable properties, path (as with all CGPathRef animatable properties) does not support implicit animation.
Explicit vs. Implicit animations
An "explicit" animation is an animation object (e.g. CABasicAnimation) that is explicitly added to the layer by calling -addAnimation:forKey: on the layer.
An "implicit" animation is an animation that happens implicitly as a result of changing an animatable property.
The animation is considered implicit even if the property is changed within a transaction.
I am trying to animate a UIView through non linear path(i'm not trying to draw the path itself) like this :
The initial position of the view is determinated using a trailing and bottom constraint (viewBottomConstraint.constant == 100 & viewTrailingConstraint.constant == 300)
I am using UIView.animatedWithDuration like this :
viewTrailingConstraint.constant = 20
viewBottomConstraint.constant = 450
UIView.animateWithDuration(1.5,animation:{
self.view.layoutIfNeeded()
},completition:nil)
But it animate the view in a linear path.
You can use keyFrame animation with path
let keyFrameAnimation = CAKeyframeAnimation(keyPath:"position")
let mutablePath = CGPathCreateMutable()
CGPathMoveToPoint(mutablePath, nil,50,200)
CGPathAddQuadCurveToPoint(mutablePath, nil,150,100, 250, 200)
keyFrameAnimation.path = mutablePath
keyFrameAnimation.duration = 2.0
keyFrameAnimation.fillMode = kCAFillModeForwards
keyFrameAnimation.removedOnCompletion = false
self.label.layer.addAnimation(keyFrameAnimation, forKey: "animation")
Gif
About this function
void CGContextAddQuadCurveToPoint (
CGContextRef _Nullable c,
CGFloat cpx,
CGFloat cpy,
CGFloat x,
CGFloat y
);
(cpx,cpy) is control point,and (x,y) is end point
Leo's answer of using Core Animation and CAKeyframeAnimation is good, but it operates on the view's "presentation layer" and only creates the appearance of moving the view to a new location. You'll need to add extra code to actually move the view to it's final location after the animation completes. Plus Core Animation is complex and confusing.
I'd recommend using the UIView method
animateKeyframesWithDuration:delay:options:animations:completion:. You'd probably want to use the option value UIViewKeyframeAnimationOptionCalculationModeCubic, which causes the object to move along a curved path that passes through all of your points.
You call that on your view, and then in the animation block, you make multiple calls to addKeyframeWithRelativeStartTime:relativeDuration:animations: that move your view to points along your curve.
I have a sample project on github that shows this and other techniques. It's called KeyframeViewAnimations (link)
Edit:
(Note that UIView animations like animateKeyframes(withDuration:delay:options:animations:completion:) don't actually animate your views along the path you specify. They use a presentation layer just like CALayer animations do, and while the presentation layer makes the view look like it's moving along the specified path, it actually snaps from the beginning position to the end position at the beginning of the animation. UIView animations do move the view to its destination position, where CALayer animations move the presentation layer while not moving the layer/view at all.)
Another subtle difference between Leo's path-based UIView animation and my answer using UIView animateKeyframes(withDuration:delay:options:animations:completion:)is that CGPath curves are cubic or quadratic Bezier curves, and my answer animates using a different kind of curve called a Katmull-Rom spline. Bezier paths start and end at their beginning and ending points, and are attracted to, but don't pass through their middle control points. Catmull-Rom splines generate a curve that passes through every one of their control points.
I know that external change to center, bounds and transform will be ignored after UIDynamicItems init.
But I need to manually change the transform of UIView that in UIDynamicAnimator system.
Every time I change the transform, it will be covered at once.
So any idea? Thanks.
Any time you change one of the animated properties, you need to call [dynamicAnimator updateItemUsingCurrentState:item] to let the dynamic animator know you did it. It'll update it's internal representation to match the current state.
EDIT: I see from your code below that you're trying to modify the scale. UIDynamicAnimator only supports rotation and position, not scale (or any other type of affine transform). It unfortunately takes over transform in order to implement just rotation. I consider this a bug in UIDynamicAnimator (but then I find much of the implementation of UIKit Dynamics to classify as "bugs").
What you can do is modify your bounds (before calling updateItem...) and redraw yourself. If you need the performance of an affine transform, you have a few options:
Move your actual drawing logic into a CALayer or subview and modify its scale (updating your bounds to match if you need collision behaviors to still work).
Instead of attaching your view to the behavior, attach a proxy object (just implement <UIDyanamicItem> on an NSObject) that passes the transform changes to you. You can then combine the requested transform with your own transform.
You can also use the .action property of the UIDynamicBehavior to set your desired transform at every tick of the animation.
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
attachment.damping = 0.8f;
attachment.frequency = 0.8f;
attachment.action = ^{
CGAffineTransform currentTransform = item.transform;
item.transform = CGAffineTransformScale(currentTransform, 1.2, 1.2)
};
You would need to add logic within the action block to determine when the scale should be changed, and by how much, otherwise your view will always be at 120%.
Another way to fix this (I think we should call it bug) is to override the transform property of the UIView used. Something like this:
override var transform: CGAffineTransform {
set (value) {
}
get {
return super.transform
}
}
var ownTransform: CGAffineTransform. {
didSet {
super.transform = ownTransform
}
}
It's a kind of a hack, but it works fine, if you don't use rotation in UIKitDynamics.