You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何用CoreGraphics & Core Animation实现指定文本动画(附参考链接)

How to Recreate That Target Text Animation with Core Graphics

Hey there! Since you're already using Core Graphics to render text, let's build on that to nail the animation you're after. I'll walk through the core steps (assuming the animation is a sequential character reveal with either stroke drawing or positional fade-in—super common in those polished text animations):

1. First: Break Text into Animatable CGPaths

To animate individual characters, we need to extract each character's vector path and its position on screen using Core Text (it plays nicely with Core Graphics):

import CoreText
import UIKit

class AnimatedTextLayer: CALayer {
    var displayText: String = ""
    var textFont: UIFont = .systemFont(ofSize: 24, weight: .medium)
    private var characterData: [(path: CGPath, origin: CGPoint)] = []
    
    override func layoutSublayers() {
        super.layoutSublayers()
        parseTextIntoPaths()
    }
    
    private func parseTextIntoPaths() {
        characterData.removeAll()
        let attributedText = NSAttributedString(string: displayText, attributes: [
            .font: textFont,
            .foregroundColor: UIColor.label
        ])
        
        let framesetter = CTFramesetterCreateWithAttributedString(attributedText as CFAttributedString)
        let textBounds = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height)
        let textPath = CGPath(rect: textBounds, transform: nil)
        let textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedText.length), textPath, nil)
        
        guard let lines = CTFrameGetLines(textFrame) as? [CTLine] else { return }
        let lineOrigins = CTFrameGetLineOrigins(textFrame, CFRangeMake(0, lines.count), nil)
        
        for (lineIndex, line) in lines.enumerated() {
            let lineOrigin = lineOrigins[lineIndex]
            guard let glyphRuns = CTLineGetGlyphRuns(line) as? [CTRun] else { continue }
            
            for run in glyphRuns {
                let runFont = CTRunGetAttributes(run)[kCTFontAttributeName] as! CTFont
                let glyphCount = CTRunGetGlyphCount(run)
                
                for glyphIdx in 0..<glyphCount {
                    var glyph = CGGlyph()
                    var glyphPosition = CGPoint()
                    CTRunGetGlyphs(run, CFRangeMake(glyphIdx, 1), &glyph)
                    CTRunGetPositions(run, CFRangeMake(glyphIdx, 1), &glyphPosition)
                    
                    if let glyphPath = CTFontCreatePathForGlyph(runFont, glyph, nil) {
                        // Adjust position to match UIKit's coordinate system (Core Text uses flipped Y)
                        let transform = CGAffineTransform(translationX: lineOrigin.x + glyphPosition.x, 
                                                          y: lineOrigin.y + glyphPosition.y + bounds.height - textFont.capHeight)
                        let adjustedPath = glyphPath.copy(using: transform)!
                        characterData.append((path: adjustedPath, origin: CGPoint(x: lineOrigin.x + glyphPosition.x, y: lineOrigin.y + glyphPosition.y)))
                    }
                }
            }
        }
    }
}

2. Add the Animation Logic

Now let's animate each character layer. Depending on the exact animation you want (stroke draw, fade-in with slide, etc.), tweak the animations below:

For a Stroke + Fade-In Sequential Animation

extension AnimatedTextLayer {
    func triggerAnimation() {
        // Clear old layers first
        sublayers?.forEach { $0.removeFromSuperlayer() }
        
        for (charIndex, charInfo) in characterData.enumerated() {
            let shapeLayer = CAShapeLayer()
            shapeLayer.path = charInfo.path
            shapeLayer.strokeColor = UIColor.label.cgColor
            shapeLayer.fillColor = UIColor.label.cgColor
            shapeLayer.lineWidth = 1
            shapeLayer.strokeEnd = 0
            shapeLayer.opacity = 0
            addSublayer(shapeLayer)
            
            // Stroke animation (draws the character)
            let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
            strokeAnim.fromValue = 0
            strokeAnim.toValue = 1
            strokeAnim.duration = 0.3
            
            // Fade-in animation
            let fadeAnim = CABasicAnimation(keyPath: "opacity")
            fadeAnim.fromValue = 0
            fadeAnim.toValue = 1
            fadeAnim.duration = 0.2
            
            // Group animations and add delay for sequential effect
            let animGroup = CAAnimationGroup()
            animGroup.animations = [strokeAnim, fadeAnim]
            animGroup.duration = 0.3
            animGroup.beginTime = CACurrentMediaTime() + Double(charIndex) * 0.1
            animGroup.fillMode = .forwards
            animGroup.isRemovedOnCompletion = false
            
            shapeLayer.add(animGroup, forKey: "charAnim_\(charIndex)")
        }
    }
}

If You Want a Slide + Fade-In Instead

Replace the animation group with this to make characters slide up from below:

// Slide animation (move from bottom to target position)
let slideAnim = CABasicAnimation(keyPath: "position.y")
slideAnim.fromValue = charInfo.origin.y + 20
slideAnim.toValue = charInfo.origin.y + bounds.height - textFont.capHeight
slideAnim.duration = 0.3

// Fade-in animation
let fadeAnim = CABasicAnimation(keyPath: "opacity")
fadeAnim.fromValue = 0
fadeAnim.toValue = 1
fadeAnim.duration = 0.3

let animGroup = CAAnimationGroup()
animGroup.animations = [slideAnim, fadeAnim]
animGroup.duration = 0.3
animGroup.beginTime = CACurrentMediaTime() + Double(charIndex) * 0.1
animGroup.fillMode = .forwards
animGroup.isRemovedOnCompletion = false

3. Use It in Your View Controller

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        let animatedLayer = AnimatedTextLayer()
        animatedLayer.frame = CGRect(x: 40, y: 250, width: view.bounds.width - 80, height: 120)
        animatedLayer.displayText = "Your Animated Text Here"
        animatedLayer.textFont = .systemFont(ofSize: 40, weight: .bold)
        view.layer.addSublayer(animatedLayer)
        
        // Trigger animation after a short delay
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
            animatedLayer.triggerAnimation()
        }
    }
}

Quick Troubleshooting Tips

  • If your text is offset incorrectly: Double-check the coordinate transform—Core Text uses a flipped Y-axis compared to UIKit, so the translation adjustment is key.
  • For custom fonts: Make sure your CTFont references the correct font file, and that you've added it to your project's info.plist.
  • If you need a curved path animation: Swap CABasicAnimation with CAKeyframeAnimation and define a custom path for characters to follow.

内容的提问来源于stack exchange,提问作者Diwakar Reddy

火山引擎 最新活动