如何为 CATextLayer 的字符串设置动画?

How to animate the string of a CATextLayer?

我目前正在使用 AVMutableComposition 在视频中叠加一些文本,我想根据某个时间间隔重复更改字符串。我正在尝试使用核心动画来实现这一点;但是,字符串 属性 似乎不可设置动画。有没有其他方法可以达到目的?谢谢。

代码(无效):

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
}

字符串不是 CATextLayer 上的动画路径。这是您可以在任何 CALayer 上使用的 list of keyPaths

关于将 Core Animation 与 AVFoundation 一起使用的一些其他重要事项。

  • 所有的动画都得去掉false的完成。
  • 一个图层只能应用一次关键路径动画。这意味着如果你想添加淡入和淡出的不透明度,它需要组合成一个关键帧动画。在核心动画中,您可以使用 beginTime 并应用两个不同的不透明度动画,但根据我使用 AVFoundation 和核心动画的经验,这不起作用。因此,如果您想在 CALayer 的同一键路径上使用两个不同的(不透明度)动画,您将必须计算出您希望发生的键时间和值。
  • 您的离散曲线仍然有效,但您必须计算出关键时间和值。
  • 因为字符串不可用作动画 属性 下一个最好的办法是使用多个 CATextLayer 并将它们一个接一个地淡入淡出。这是一个例子。将 CACurrentMediaTime() 替换为 AVCoreAnimationBeginTimeAtZero 以便与 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
    }
}

示例 2 - 使用长淡入淡出的 Gif 附件

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
    }
}

结果: 持续时间为 2 秒进出 2 秒。可以是即时的。

试想一下电影是怎么加字幕的?

字幕文件包含时间戳值,例如 00:31:21 : "Some text"。因此,当电影的搜索栏位于 00:31:21 时,您会看到 "Some text" 作为副标题。

与您的要求类似,您需要多个 CATextLayers 具有与之对应的动画(每个 CAExtLayer 相同或不同)。当一个 CATextLayer 动画结束时,您可以淡入第二个 CATextLayer 并淡出第一个。

我记得上次这样做,动画可以分组,您实际上可以指定动画的起点。检查 beginTime 属性 共 CABasicAnimation

Swift 5 的解决方案:

喜欢@agibson007 的回答

针对 0.25 的持续时间进行了优化,更快是不可能的。 (褪色/闪烁问题)

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)

使用 CATextLayer 和 CAKeyframeAnimation 的文本动画解决方案。现在字符串 属性 是可以动画的。不知道以前怎么样。

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);
}