如何给CATextLayer中的字符串添加动画效果?

4

我正在使用AVMutableComposition向视频中叠加一些文本,我希望根据一定时间间隔反复更改字符串。我尝试使用Core Animation来实现这个目标;但是,字符串属性似乎不可动画化。是否有其他方法可以实现这个目标?谢谢。

代码(不起作用):

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
}

你想要改变字符串的文本还是让字符串本身动起来? - nr5
@nr5 我想要改变字符串的文本内容。 - Bo Ni
你有没有检查过我的答案? - agibson007
4个回答

9

在CATextLayer上,字符串不是可动画路径。以下是可以用于任何CALayer的关键路径列表

使用AVFoundation时需要注意以下几点:

  • 所有动画都必须具有false的removedCompletion。
  • 您只能一次向图层应用一个关键路径动画。这意味着,如果您想添加一个淡入淡出的不透明度,它需要被合并成一个关键帧动画。在核心动画中,您可以使用beginTime并应用两个不同的不透明度动画,但在我使用AVFoundation和核心动画的经验中,这样做行不通。因此,如果您想在同一关键路径CALayer上使用两种不同(例如不透明度)动画,则必须找出所需发生的关键时间和值。
  • 您的离散曲线仍将起作用,但您需要解决关键时间和值。
  • 由于字符串不可用作可动画属性,下一个最好的方法是使用多个CATextLayers并逐个淡入它们。这是一个示例。将CACurrentMediaTime()替换为AVCoreAnimationBeginTimeAtZero以与AVFoundation一起使用。这只是一个示例,让您可以想象您想要的东西。

示例1:使用离散曲线

 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秒时长的进场和出场。可以是立即的。 resultgif


可以立即完成。你如何使文本立即切换,而不是淡出。 - user728881
@user728881 请参考上面的示例1。它是立即显示的,没有淡入淡出效果。示例2是gif动画。 - agibson007
谢谢你测试了它,我正在努力调整时间。 假设我有 ["1","2","3"...."100"] 并且我想每 0.1 秒更新一次,而不会出现字符串重叠。因此,在这种情况下,最终结果就成为屏幕上的平滑计时器。 - user728881
1
@user728881,根据您的需求,您应该将关键时间设置为animation.keyTimes = [0,0.00000001,0.998,0.999,1],持续时间为0.1,间距为0。将字符串1...100构造成一个数组并尝试使用它。我刚刚测试了一下,效果还不错。 - agibson007
持续时间为0.25的最佳结果是keyTimes = [0,0.00000001,0.999,0.999995,1],间距为0。 - Skyborg

3

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

如果将 beginTime 设置为 > 0,则似乎无法正常工作。否则它可以正常运行。 - K.Rzech
1
在我的测试中,AVSynchronizedLayer 运行良好,但 AVVideoCompositionCoreAnimationTool 则不行。当我导出时,字符串值无法及时更新。AVVideoCompositionCoreAnimationTool 无法使用某些 keyPaths(透明度可以工作)?我在苹果的开发者论坛上找到了一个类似的问题,但没有答案:https://developer.apple.com/forums/thread/111317 - YoonHo

2

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)

0

试着想一下电影中是如何添加字幕的?

字幕文件包含时间戳值,例如00:31:21:“一些文本”。因此,当电影的进度条在00:31:21时,您会看到“一些文本”作为字幕。

同样地,对于您的要求,您需要多个CATextLayers,它们具有相应的动画(每个CATExtLayer相同或不同)。当一个CATextLayer动画结束时,您可以淡入第二个CATextLayer并淡出第一个CATextLayer。

我上次做这件事时记得,动画可以分组,您实际上可以指定动画的起始点。检查CABasicAnimationbeginTime属性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接