Reputation: 595
I am currently overlaying some text in a video using the AVMutableComposition, I would like to change the string repeatedly based on some time interval. I am trying to use the Core Animation to achieve this; however, the string property does not seem to be animatable. Is there any other way to achieve the goal? Thanks.
Code (not working):
func getSubtitlesAnimation(withFrames frames: [String], duration: CFTimeInterval)->CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath:"string")
animation.calculationMode = kCAAnimationDiscrete
animation.duration = duration
animation.values = frames
animation.keyTimes = [0,0.5,1]
animation.repeatCount = Float(frames.count)
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeForwards
animation.beginTime = AVCoreAnimationBeginTimeAtZero
return animation
}
Upvotes: 4
Views: 3332
Reputation: 61
Solution for text animation using CATextLayer and CAKeyframeAnimation. Right now the string property is animate-able. Idk how was it in the past.
func addTextLayer(to layer: CALayer) {
let myAnimation = CAKeyframeAnimation(keyPath: "string");
myAnimation.beginTime = 0;
myAnimation.duration = 1.0;
myAnimation.values = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
myAnimation.fillMode = CAMediaTimingFillMode.forwards;
myAnimation.isRemovedOnCompletion = false;
myAnimation.repeatCount = 1;
let textLayer = CATextLayer();
textLayer.frame = CGRect(x: 200, y: 300, width: 100, height: 100);
textLayer.string = "0";
textLayer.font = UIFont.systemFont(ofSize: 20)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.fontSize = 40.0
textLayer.alignmentMode = .center
textLayer.add(myAnimation, forKey: nil);
layer.addSublayer(textLayer);
}
Upvotes: 3
Reputation: 874
Solution for Swift 5:
like the answer from @agibson007
optimized for a duration of 0.25, faster wasn´t possible. (problems with fading / flicker)
func animateText(subtitles:[String],duration:Double,animationSpacing:Double,frame:CGRect,targetLayer:CALayer){
var currentTime : Double = 0
for x in 0..<subtitles.count{
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = subtitles[x]
textLayer.font = UIFont.systemFont(ofSize: 20)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.fontSize = 40.0
textLayer.alignmentMode = .center
let anim = getSubtitlesAnimation(duration: duration, startTime: currentTime)
textLayer.add(anim, forKey: "opacityLayer\(x)")
targetLayer.addSublayer(textLayer)
currentTime += duration + animationSpacing
}
}
func getSubtitlesAnimation(duration: CFTimeInterval,startTime:Double)->CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath:"opacity")
animation.duration = duration
animation.calculationMode = .discrete
animation.values = [0,1,1,0,0]
animation.keyTimes = [0,0.00000001,0.999,0.999995,1]
animation.isRemovedOnCompletion = false
animation.fillMode = .both
animation.beginTime = AVCoreAnimationBeginTimeAtZero + startTime // CACurrentMediaTime() <- NO AV Foundation
return animation
}
let steps = 0.25
let duration = 8.0
let textFrame = CGRect( x: (videoSize.width / 2) - (90 / 2) , y: videoSize.height * 0.2, width: 90, height: 50)
var subtitles:[String] = []
for i in 0...Int(duration / steps) {
let time = i > 0 ? steps * Double(i) : Double(i)
subtitles.append(String(format: "%0.1f", time) )
}
animateText(subtitles: subtitles, duration: steps, animationSpacing: 0, frame: textFrame, targetLayer: layer)
Upvotes: 2
Reputation: 4325
Try to think how subtitles are added in a movie?
A subtitle file contains time stamp values like, 00:31:21 : "Some text". So when the seekbar of a movie is at 00:31:21, you see "Some text" as the subtitle.
Similarly fo your requirement, you would need multiple CATextLayers
which have animations corresponding to them (same or different for each CATExtLayer). When one CATextLayer animation ends, you can fade in your second CATextLayer and Fadeout the first one.
Last time I did this I remember, animations can be grouped and you can actually specify start point of an animation. Check beginTime
property of CABasicAnimation
Upvotes: 0
Reputation: 4373
String is not an animatable path on a CATextLayer. Here is the list of keyPaths that you can use on any CALayer.
Some other important things about using Core Animation with AVFoundation.
Example1Using-Discrete
import UIKit
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//center the frame
let textFrame = CGRect(x: (self.view.bounds.width - 200)/2, y: (self.view.bounds.height - 100)/2, width: 200, height: 50)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
//animation spacing could be a negative value of half the animation to appear to fade between strings
self.animateText(subtitles: ["Hello","Good Morning","Good Afternoon","Good Evening","Goodnight","Goodbye"], duration: 2, animationSpacing: 0, frame: textFrame, targetLayer: self.view.layer)
}
}
func animateText(subtitles:[String],duration:Double,animationSpacing:Double,frame:CGRect,targetLayer:CALayer){
var currentTime : Double = 0
for x in 0..<subtitles.count{
let string = subtitles[x]
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = string
textLayer.font = UIFont.systemFont(ofSize: 20)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.fontSize = 20.0
textLayer.alignmentMode = kCAAlignmentCenter
let anim = getSubtitlesAnimation(duration: duration, startTime: currentTime)
targetLayer.addSublayer(textLayer)
textLayer.add(anim, forKey: "opacityLayer\(x)")
currentTime += duration + animationSpacing
}
}
func getSubtitlesAnimation(duration: CFTimeInterval,startTime:Double)->CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath:"opacity")
animation.duration = duration
animation.calculationMode = kCAAnimationDiscrete
//have to fade in and out with a single animation because AVFoundation
//won't allow you to animate the same propery on the same layer with
//two different animations
animation.values = [0,1,1,0,0]
animation.keyTimes = [0,0.001,0.99,0.999,1]
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeBoth
//Replace with AVCoreAnimationBeginTimeAtZero for AVFoundation
animation.beginTime = CACurrentMediaTime() + startTime
return animation
}
}
EXAMPLE2-Using a long fade-Gif Attached
import UIKit
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//center the frame
let textFrame = CGRect(x: (self.view.bounds.width - 200)/2, y: (self.view.bounds.height - 100)/2, width: 200, height: 50)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
//animation spacing could be a negative value of half the animation to appear to fade between strings
self.animateText(subtitles: ["Hello","Good Morning","Good Afternoon","Good Evening","Goodnight","Goodbye"], duration: 4, animationSpacing: -2, frame: textFrame, targetLayer: self.view.layer)
}
}
func animateText(subtitles:[String],duration:Double,animationSpacing:Double,frame:CGRect,targetLayer:CALayer){
var currentTime : Double = 0
for x in 0..<subtitles.count{
let string = subtitles[x]
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = string
textLayer.font = UIFont.systemFont(ofSize: 20)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.fontSize = 20.0
textLayer.alignmentMode = kCAAlignmentCenter
let anim = getSubtitlesAnimation(duration: duration, startTime: currentTime)
targetLayer.addSublayer(textLayer)
textLayer.add(anim, forKey: "opacityLayer\(x)")
currentTime += duration + animationSpacing
}
}
func getSubtitlesAnimation(duration: CFTimeInterval,startTime:Double)->CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath:"opacity")
animation.duration = duration
//have to fade in and out with a single animation because AVFoundation
//won't allow you to animate the same propery on the same layer with
//two different animations
animation.values = [0,0.5,1,0.5,0]
animation.keyTimes = [0,0.25,0.5,0.75,1]
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeBoth
//Replace with AVCoreAnimationBeginTimeAtZero for AVFoundation
animation.beginTime = CACurrentMediaTime() + startTime
return animation
}
}
RESULT:
Duration of 2 seconds in and 2 seconds out. Could be immediate.
Upvotes: 10