How can I save each frame of a CoreAnimation based animation (as image files)?
Here's my little playground scene. The animation lasts 2.4 seconds.
let stage = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 300, height: 300))
stage.backgroundColor = UIColor.blueColor();
var dot = UIView(frame: CGRectMake(0, 0, 10, 10))
dot.backgroundColor = UIColor.redColor()
dot.center = stage.center
UIView.animateWithDuration(2.4, animations: { () -> Void in
dot.center.y = dot.center.y + 50
})
I know how to save a static UIView frame as PDF but I am not sure how to hook into the animation sequence while it happens and capture/save the view frame by frame.
Option 1
As far as I see it I need to hook into the animation block and save the stage view for each frame (at a given frame rate?). How can I do this? Is there some sort of callback I can use?
Option 2
While looking for a solution I came across another option that looks even more promising (based on this question and this awesome blog post). Using a CBAnimation, setting a timingFunction and then setting the timeOffset to the progress states I want to render.
Here's an example for the frame at 50% progress (using a different example here).
var drawAnimation = CABasicAnimation(keyPath: "strokeEnd")
…
drawAnimation.fromValue = NSNumber(float: 0.0)
drawAnimation.toValue = NSNumber(float: 1.0)
drawAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
circle.addAnimation(drawAnimation, forKey: "drawCircleAnimation")
circle.speed = 0.0;
circle.timeOffset = 0.5
The problem is that I cannot capture this timeOffset based animation state. When I render the view I only get the final state instead of the frozen animation frame at 0.5.
Any help to resolve this is appreciated.
Related
I've created core animation using 40 images (Per image stays in CALayer exactly 3 seconds).
Total duration = 120 seconds. Each photo move from left to right with the help of CABasicAnimation(keypath : "position.x"). Now I've an UISlider. I want to update slider's value automatically when core animation is playing in CALayer. I used Timer to update slider's value like this
time = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateSlideValue), userInfo: nil, repeats: true)
Animation Code :
let blackLayer = CALayer()
blackLayer.frame = CGRect(x: -viewSize.width, y: 0, width: viewSize.width, height: viewSize.height)
let imageLayer = CALayer()
imageLayer.frame = CGRect(x: 0, y: 0, width: viewSize.width, height: viewSize.height)
imageLayer.contents = nextPhoto?.cgImage
blackLayer.addSublayer(imageLayer)
let animation = CABasicAnimation()
animation.keyPath = "position.x"
animation.fromValue = -viewSize.width
animation.toValue = (viewSize.width)/2
animation.duration = self.perImageLife
animation.speed = 1 // 1 is default actually
animation.beginTime = CACurrentMediaTime() + animationTimes[index-1] // animationTimes is an array like [0,3,6,9,.....]
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.delegate = self
blackLayer.add(animation, forKey: "basic")
parentLayer.addSublayer(blackLayer)
But it's not a better idea to update using Timer. How to update slider's value automatically when core animation is running without Timer?
Here is a small recorded video of my project. https://drive.google.com/file/d/1SjaWJM_FEWp1TyR7fpQt8_IZdme_I2Vc/view?usp=sharing
I don't think that you can observe the properties you are animating and use those changing values to update the slider position. You'll have to use a timer to update the slider value at the moment of each frame change as you are doing.
Do you intend for the user to be able to drag the "thumb" on the slider to scrub the animation back and forth? That's another layer of complexity to the problem.
If that is your intent I would suggest using a UIViewPropertyAnimator rather than using a CAAnimation. Pausing/resuming/scrubbing a CAAnimation is tricky and not terribly well documented. I've worked it out in the past, so if you are determined to go that route I can dredge up some old sample code (probably written in Objective-C) and try to walk you through it.
This project on Github (from 7 years ago!) implements both UIView and CAAnimation-based animations that include sliders that show their progress and can scrub back and forth.
It predates UIViewPropertyAnimator however. I also wrote a much simpler demo of UIViewPropertyAnimator when it first came out. It is probably written for an older version of Swift however.
I want to create one rectangle layer of progress bar around my object it maybe anything it should be an uibutton, uiview or any object so i create a below code:
let layer_ca = CAShapeLayer.init()
layer_ca.strokeColor = UIColor.green.cgColor
layer_ca.lineWidth = 10.0
layer_ca.fillColor = UIColor.clear.cgColor
let bezier_path = UIBezierPath.init(roundedRect: CGRect.init(x: captureButton.frame.origin.x, y: captureButton.frame.origin.y, width: captureButton.frame.width, height: captureButton.frame.height), cornerRadius: 15)
bezier_path.move(to: CGPoint(x: 12, y: 15))
layer_ca.path = bezier_path.cgPath
let animation = CABasicAnimation.init(keyPath: "strokeEnd")
animation.fromValue = NSNumber.init(value: 0.0)
animation.toValue = NSNumber.init(value: 1.0)
animation.duration = 2.0
layer_ca.add(animation, forKey: "myStroke")
self.view.layer.addSublayer(layer_ca)
here "captureButton" was an my object so its created successful above the object with the cabasicanimation and now what my question was how to change the ending point of the path and how should i stop the path to half of the distance path and then i continue with the same path which i stop previous path whenever i call to start.
i need exactly like as a rectangular progress bar around an object.
please help me anyone who should known the answer..!
I just have the same question, and spend a few hours to figure it out. if you want control the start point or end point, you should create your own BezierPath, and custom your path. bezierPath.move("start position"), and there it is, the last point will be the end position.
I work on a UI component that implements flip-card clock animation. All works fine, but when I change a top CALayer contents to new image, the old image stays visible before changeding. It creates confusion effect. For better explanation I place the gif animation bellow:
This is code with changing a CALayer contents:
firstTopLayer.contents = secondTopLayer.contents
let bottomAnim = CABasicAnimation(keyPath: "transform")
bottomAnim.duration = animDuration/2
bottomAnim.repeatCount = 1
bottomAnim.fromValue = NSValue.init(caTransform3D:
CATransform3DMakeRotation((CGFloat)(M_PI_2), 1, 0, 0))
bottomAnim.toValue = NSValue.init(caTransform3D:
CATransform3DMakeRotation(0, 1, 0, 0))
bottomAnim.isRemovedOnCompletion = true
bottomAnim.timingFunction = CAMediaTimingFunction.init(name: kCAMediaTimingFunctionEaseIn)
firstBottomLayer.add(bottomAnim, forKey: "bottom")
firstBottomLayer.contents = self.bufferContents
For more information I place a link to the repository
I found a solution. Top animation must have this configuration
topAnim.fillMode = kCAFillModeForwards
topAnim.isRemovedOnCompletion = false
and after each start this animation.
firstTopLayer.removeAnimation(forKey: kTopAnimaton)
With this configuration the top layer stays in it last frame animation position
I am trying to animate the opacity of a CALayer but the values are more than one value to begin with and one to end with.
For example: I want it to animate throw these values: 0.0, 0.7, 0.3, 1.0, 0.5, 0.0
I also want the animation to repeat with auto reverse.
This is what I have for now:
let redLineAnimation = CABasicAnimation(keyPath: "opacity")
redLineAnimation.duration = 0.4
redLineAnimation.autoreverses = true
redLineAnimation.fromValue = 0.0
redLineAnimation.toValue = 1.0
redLineAnimation.repeatCount = Float.infinity
movingRedLineLayer.addAnimation(redLineAnimation, forKey: nil)
I am new to iOS development. What can I do? Thanks!
Take a look at CAKeyframeAnimation. That will give you what you want.
And if what you are animating is a view's layer, you could use the easier-to-use UIView keyframe based animation methods. (Core Animation is pretty tricky, and the docs are spotty)
I have a collection view with a collection of images (which are views). The layout causes transformations to occur on every visible item. The item at the center is both zoomed and translated while the others are just translated.
The point is, the layout is setting the layer's transform property for each item.
Later, when the user touches an item, I want to animate the item using a keyframe animation.
The behavior I am experiencing is that the item seems to revert back to its untransformed state, the animation occurs on its untransformed state and then the item's layout transformation returns.
Why?
I've tried using the view's transform property, the backing layer's affine transform property, and the backing layer's 3D transform property all with this same behavior. The keyframe animation uses 3D transforms.
I clearly have a gap in my understanding of transforms and animations, though I have seen this question posed for platforms other than iOS without answers.
Layout transformation code (in a flow layout subclass):
// Calculate the distance from the center of the visible rect to the center of the attributes.
// Then normalize it so we can compare them all. This way, all items further away than the
// active get the same transform.
let distanceFromVisibleRectToItem = CGRectGetMidX(visibleRect) - attributes.center.x
let normalizedDistance = distanceFromVisibleRectToItem / ACTIVE_DISTANCE
let isLeft = distanceFromVisibleRectToItem > 0
var transform = CATransform3DIdentity
var maskAlpha: CGFloat = 0.0
if (abs(distanceFromVisibleRectToItem) < ACTIVE_DISTANCE) {
// We're close enough to apply the transform in relation to how far away from the center we are.
transform = CATransform3DTranslate(transform,
(isLeft ? -FLOW_OFFSET : FLOW_OFFSET) * abs(distanceFromVisibleRectToItem/TRANSLATE_DISTANCE),
0, (1 - abs(normalizedDistance)) * 40000 + (isLeft ? 200 : 0))
// Set the zoom factor.
let zoom = 1 + ZOOM_FACTOR * (1 - abs(normalizedDistance))
transform = CATransform3DScale(transform, zoom, zoom, 1)
attributes.zIndex = 1
let ratioToCenter = (ACTIVE_DISTANCE - abs(distanceFromVisibleRectToItem)) / ACTIVE_DISTANCE
// Interpolate between 0.0f and INACTIVE_GREY_VALUE
maskAlpha = INACTIVE_GREY_VALUE + ratioToCenter * (-INACTIVE_GREY_VALUE)
} else {
// We're too far away - just apply a standard perspective transform.
transform = CATransform3DTranslate(transform, isLeft ? -FLOW_OFFSET : FLOW_OFFSET, 0, 0)
attributes.zIndex = 0
maskAlpha = INACTIVE_GREY_VALUE
}
attributes.transform3D = transform
Animation code (note this is a Swift extension to the UIView class):
func bounceView(completion: (() -> Void)? = nil) {
var animation = bounceAnimation(frame.height)
animation.delegate = AnimationDelegate(completion: completion)
layer.addAnimation(animation, forKey: "bounce")
}
func bounceAnimation(itemHeight: CGFloat) -> CAKeyframeAnimation {
let factors: [CGFloat] = [0, 32, 60, 83, 100, 114, 124, 128, 128, 124, 114, 100, 83, 60, 32,
0, 24, 42, 54, 62, 64, 62, 54, 42, 24, 0, 18, 28, 32, 28, 18, 0]
var transforms: [AnyObject] = [NSValue(CATransform3D: self.layer.transform)]
for factor in factors {
let positionOffset = factor/256.0 * itemHeight
let transform = CATransform3DMakeTranslation(0, -positionOffset, 0)
transforms.append(NSValue(CATransform3D: transform))
}
let animation = CAKeyframeAnimation(keyPath: "transform")
animation.repeatCount = 1
animation.duration = CFTimeInterval(factors.count)/30.0
animation.fillMode = kCAFillModeForwards
animation.values = transforms
animation.removedOnCompletion = true // final stage is equal to starting stage
animation.autoreverses = false
return animation;
}
Note: I should state more simply that I want this process to begin and end in the collection view layout transformed state. I want the animation to occur on the layout transformed state as well (i.e. no reverting to the untransformed state at all).
It seems that in your animations you are not syncing your model with your presentation layer.
When you animate with Core Animation (CABasicAnimation, for instance) it only changes the presentation layer (the thing you see on the screen) but in fact the state your layers have is different from the visible one.
(You can read more about that in this objc.io issue)
So basically to fix this you need to update your model in your CAAnimation. You need to set a from and toValue, and then, after you set those, you update your transform so it can match the final state of your presentation layer.
EDIT
Now that we have some code I can be concrete
To sync the model layer to the presentation layer you need to set your model to the final state before adding the animation:
var animation = bounceAnimation(frame.height)
animation.delegate = AnimationDelegate(completion: completion)
layer.transform = CATransform3DMakeTranslation(0, 0, 0)
layer.addAnimation(animation, forKey: "bounce")
David Rönnqvist has a good rant about this issue in this gist.
SOLUTION:
The help Tiago Almeida offered was very useful to get me thinking about what the animation was actually doing (how it was actually working). My basic assumption that the animation would begin with the current model layer transform was incorrect. As Tiago points out, the presentation layer is a separate and distinct layer from the model layer.
Once I got that through my thick head, I realized that I had to manually concatenate the current model transform with the transform being built for the animation frame and viola! Things work as expected.
The updated animation code:
var transforms: [AnyObject] = []
for factor in factors {
let positionOffset = factor/256.0 * itemHeight
var transform = CATransform3DMakeTranslation(0, -positionOffset, 0)
transform = CATransform3DConcat(self.layer.transform, transform)
transforms.append(NSValue(CATransform3D: transform))
}