Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to animate the string of a CATextLayer?

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
}
like image 342
Bo Ni Avatar asked Oct 21 '25 06:10

Bo Ni


2 Answers

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.

  • All animations have to have removedCompletion of false.
  • You can only apply one key path animation to a layer once. Meaning if you want to add an opacity of fade in and fade out it needs to be combined into a single keyframe animation. In core animation you could use beginTime and apply two different opacity animations but in my experience with AVFoundation and Core Animation this does not work. Because of this you will have to figure out the key times and values for what you would want to occur if you wanted to use two different (ex opacity) animations on the same keypath for the CALayer.
  • Your discrete curve will still work but you will have to work out the keytimes and values.
  • Because string is not available as an animatable property the next best thing would be to use multiple CATextLayers and fade them in one after the other. Here is an example. Replace CACurrentMediaTime() with AVCoreAnimationBeginTimeAtZero for use with AVFoundation. This was just an example so you could visualize what you want.

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. resultgif

like image 177
agibson007 Avatar answered Oct 23 '25 22:10

agibson007


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);
}
like image 25
K.Rzech Avatar answered Oct 23 '25 21:10

K.Rzech