如何用CoreGraphics & Core Animation实现指定文本动画(附参考链接)
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
CTFontreferences the correct font file, and that you've added it to your project's info.plist. - If you need a curved path animation: Swap
CABasicAnimationwithCAKeyframeAnimationand define a custom path for characters to follow.
内容的提问来源于stack exchange,提问作者Diwakar Reddy




