Reputation: 2041
I'm developing a keyframe animation editor for iOS that allows the user to create simple animations and view them, plotted against a timeline. The user is able to drag the timeline in order to change the current time.
This means that I need to be able to start an animation at a user specified time.
Whilst I can achieve this behaviour, i also am experiencing an annoying glitch every time I re-add the animation to the layer. This glitch causes the first frame of the animation to flash very quickly before CoreAnimation respects the begin time. I can mitigate the effects of this ever so slightly by setting the layer.alpha to 0 before the flush, and 1 after the flush however this still results in a nasty single frame(ish) flash!
I have recreated a sample view controller that demonstrates the code required to "change the time" of the running animation but as this project is so simple, you don't really see the negative effects of the flush: https://gist.github.com/chrisbirch/5cafca50804cf9d778ccd0fdc9e68d56
Basic idea behind the code is as follows:
Each time the user changes the current time, I restart the animation and fiddle with the CALayer timing properties like so (addStoppedAnimation:line 215):
ani = createGroup()
animatableLayer.speed = 0
animatableLayer.add(ani, forKey: "an animation key")
let time = CFTimeInterval(slider.value)
animatableLayer.timeOffset = 0
animatableLayer.beginTime = animatableLayer.superlayer!.convertTime(CACurrentMediaTime(), from: nil) - time
CATransaction.flush()
animatableLayer.timeOffset = time// offset
print("Time changed \(time)")
The glitch is caused by me having to call CATransaction.flush right before I set timeOffset. Failure to call this flush results in the begin time being ignored it would seem.
I feel like I have scoured the entire internet looking for a solution to this problem but alas Im think I'm stuck.
My question is this:
Can anyone shed any light onto why it is I need to call CATransaction.flush in order for the beginTime value that I set to take effect? Looking at apple code I didn't ever see them using flush for this purpose so perhaps I have got something obvious wrong!
Many thanks in advance
Chris
Upvotes: 0
Views: 1453
Reputation: 4393
Using your test code from the gist I have updated it to check for an animation so that there is no need to readd it. You could us a unique ID to track all animations and store it in a dictionary with view attributes. I did not implement this part but that is how I would do it. Hopefully I understood your issue enough. Also I used Xcode 9 and I am not sure of the code differences. I changed a few logic pieces so let me know if this fixes the issue.
UUID().uuidString //for unique string in implementation
//from your code just slightly altered.
//
// ViewController.swift
// CATest
//
// CATestViewController.swift
// SimpleCALayerTest
import UIKit
class CATestViewController: UIViewController, CAAnimationDelegate{
var slider : UISlider!
var animatableLayer : CALayer!
var animationContainerView : UIView!
var centerY : CGFloat!
var startTranslationX : CGFloat!
var endTranslationX : CGFloat!
let duration = 10.0
///boring nibless view setup code
override func loadView() {
let marginX = CGFloat(10)
let marginY = CGFloat(10)
let view = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
view.backgroundColor = .lightGray
slider = UISlider(frame: CGRect(x: marginX, y: 0, width: 200, height: 50))
slider.maximumValue = Float(duration)
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderDragStart(_:)), for: .touchDown)
slider.addTarget(self, action: #selector(sliderDragEnd(_:)), for: .touchUpInside)
//A view to house an animated sublayer
animationContainerView = UIView(frame: CGRect(x: marginX, y: 50, width: 200, height: 70))
//add a play button that will allow the animation to be played without hindrance from the slider
let playButton = UIButton(frame: CGRect(x: marginX, y: animationContainerView.frame.maxY + marginY, width: 200, height: 50))
playButton.setTitle("Change Frame", for: .normal)
playButton.addTarget(self, action: #selector(playAnimation), for: .touchUpInside)
view.addSubview(playButton)
//add a stopped ani button that will allow the animation to be played using slider
let addStoppedAniButton = UIButton(frame: CGRect(x: playButton.frame.origin.x, y: playButton.frame.maxY + marginY, width: playButton.frame.width, height: playButton.frame.size.height))
addStoppedAniButton.setTitle("Pause", for: .normal)
addStoppedAniButton.addTarget(self, action: #selector(cmPauseTapped(_:)), for: .touchUpInside)
view.addSubview(addStoppedAniButton)
let animatableLayerWidth = animationContainerView.bounds.width / CGFloat(4)
centerY = animationContainerView.bounds.midY
startTranslationX = animatableLayerWidth / CGFloat(2)
endTranslationX = animationContainerView.bounds.width - animatableLayerWidth / CGFloat(2)
animationContainerView.backgroundColor = .white
animationContainerView.layer.borderColor = UIColor.black.withAlphaComponent(0.5).cgColor
animationContainerView.layer.borderWidth = 1
view.addSubview(slider)
view.addSubview(animationContainerView)
//Now add a layer to animate to the container
animatableLayer = CALayer()
animatableLayer.backgroundColor = UIColor.yellow.cgColor
animatableLayer.borderWidth = 1
animatableLayer.borderColor = UIColor.black.withAlphaComponent(0.5).cgColor
var r = animationContainerView.bounds.insetBy(dx: 0, dy: 4)
r.size.width = animatableLayerWidth
animatableLayer.frame = r
animationContainerView.layer.addSublayer(animatableLayer)
self.view = view
}
@objc func cmPauseTapped(_ sender : UIButton){
if animatableLayer.speed == 0{
resume()
}else{
pause()
}
}
@objc func sliderChanged(_ sender: UISlider){
if animatableLayer.speed == 0{
let time = CFTimeInterval(sender.value)
animatableLayer.speed = 0
animatableLayer.timeOffset = time// offset
print("Time changed \(time)")
}
}
var animations = [CAAnimation]()
func addAnimations(){
let ani = CAAnimation()
animations.append(ani)
}
@objc func sliderDragStart(_ sender: UISlider)
{
if animatableLayer.speed > 0{
animatableLayer.speed = 0
}
addStoppedAnimation()
}
func pause(){
//just updating slider
if slider.value != Float(animatableLayer.timeOffset){
UIView.animate(withDuration: 0.3, animations: {
self.slider.setValue(Float(self.animatableLayer.timeOffset), animated: true)
})
}
animatableLayer.timeOffset = animatableLayer.convertTime(CACurrentMediaTime(), from: nil)
animatableLayer.speed = 0
}
func resume(){
if let _ = animatableLayer.animationKeys()?.contains("an animation key"){
animatableLayer.speed = 1.0;
let pausedTime = animatableLayer.timeOffset
animatableLayer.beginTime = 0.0;
let timeSincePause = animatableLayer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
animatableLayer.beginTime = timeSincePause;
return
}
print("Drag End with need to readd animation")
ani = createGroup()
animatableLayer.speed = 1
animatableLayer.add(ani, forKey: "an animation key")
let time = CFTimeInterval(slider.value)
animatableLayer.timeOffset = time
animatableLayer.beginTime = CACurrentMediaTime()
}
@objc func sliderDragEnd(_ sender: UISlider){
resume()
}
//Animations
var ani : CAAnimationGroup!
func createGroup() -> CAAnimationGroup
{
let ani = CAAnimationGroup()
ani.isRemovedOnCompletion = false
ani.duration = 10
ani.delegate = self
ani.animations = [createTranslationAnimation(),createColourAnimation()]
return ani
}
func createTranslationAnimation() -> CAKeyframeAnimation
{
let ani = CAKeyframeAnimation(keyPath: "position")
ani.delegate = self
ani.isRemovedOnCompletion = false
ani.duration = 10
ani.values = [CGPoint(x:0,y:centerY),CGPoint(x:endTranslationX,y:centerY)]
ani.keyTimes = [0,1]
return ani
}
func createColourAnimation() -> CAKeyframeAnimation
{
let ani = CAKeyframeAnimation(keyPath: "backgroundColor")
ani.delegate = self
ani.isRemovedOnCompletion = false
ani.duration = 10
ani.values = [UIColor.red.cgColor,UIColor.blue.cgColor]
ani.keyTimes = [0,1]
return ani
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print("Animation Stopped")
}
func animationDidStart(_ anim: CAAnimation) {
print("Animation started")
}
func addStoppedAnimation()
{
if let _ = animatableLayer.animationKeys()?.contains("an animation key"){
slider.value += 0.5
sliderChanged(slider)
return
//we do not want to readd it
}
ani = createGroup()
animatableLayer.speed = 0
animatableLayer.add(ani, forKey: "an animation key")
let time = CFTimeInterval(slider.value)
animatableLayer.timeOffset = time
animatableLayer.beginTime = CACurrentMediaTime()
}
@objc func playAnimation(){
addStoppedAnimation()
}
}
Upvotes: 1