Bo Ni
Bo Ni

Reputation: 595

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
}

Upvotes: 4

Views: 3332

Answers (4)

K.Rzech
K.Rzech

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

Skyborg
Skyborg

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

devdoe
devdoe

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

agibson007
agibson007

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.

  • 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

Upvotes: 10

Related Questions