当前位置: > > > Swift - 自动书写文字的动画效果(文字转贝塞尔曲线、实现字迹动画)

Swift - 自动书写文字的动画效果(文字转贝塞尔曲线、实现字迹动画)

    有时一些提示性的文字(如:loading...),如果能够动态地显示出来,而不是静止地放置在那里,会让页面效果更生动些。当然我们可以直接使用从左到右的遮罩动画,但对于文字显示来说又太生硬了些。

    下面介绍另一种思路,即将字符串转变成贝塞尔曲线,并通过对其添加动画,从而实现文字笔迹动态书写效果。

1,效果图

(1)在输入框中填写需要显示的文字,点击“书写”按钮,下方就会出现需要书写的文字。
(2)同时这文字不是一下就显示出来,而是会从左往右、一笔一划动态地写出来。
         

(3)注意:虽然中文也是可以书写的,但效果不如英文好。因为是绘制边框线条,所以文字是空心的。还有就是笔画顺序也不大对。
      

2,样例代码

(1)封装一个书写字迹的组件(BezierText.swift
import UIKit

class BezierText: UIView {
    
    //字迹动画时间
    private let duration:TimeInterval = 3
    
    //字迹书写图层
    private let pathLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        
        //初始化字迹图层
        pathLayer.frame = self.bounds
        pathLayer.isGeometryFlipped = true
        pathLayer.fillColor = UIColor.clear.cgColor
        pathLayer.lineWidth = 1
        pathLayer.strokeColor = UIColor.black.cgColor
        self.layer.addSublayer(pathLayer)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //动态书写指定文字
    func show(text: String) {
        //获取文字对应的贝塞尔曲线
        let textPath = bezierPathFrom(string: text)
        
        //让文字居中显示
        pathLayer.bounds = textPath.cgPath.boundingBox
        //设置笔记书写路径
        pathLayer.path = textPath.cgPath
        
        //添加笔迹书写动画
        let textAnimation = CABasicAnimation.init(keyPath: "strokeEnd")
        textAnimation.duration = duration
        textAnimation.fromValue = 0
        textAnimation.toValue = 1
        //textAnimation.repeatCount = HUGE
        pathLayer.add(textAnimation, forKey: "strokeEnd")
    }
    
    //将字符串转为贝塞尔曲线
    private func bezierPathFrom(string:String) -> UIBezierPath{
        let paths = CGMutablePath()
        let fontName = __CFStringMakeConstantString("SnellRoundhand")!
        let fontRef:AnyObject = CTFontCreateWithName(fontName, 20, nil)
        
        let attrString = NSAttributedString(string: string, attributes:
            [kCTFontAttributeName as NSAttributedStringKey : fontRef])
        let line = CTLineCreateWithAttributedString(attrString as CFAttributedString)
        let runA = CTLineGetGlyphRuns(line)
        
        for runIndex in 0..<CFArrayGetCount(runA) {
            let run = CFArrayGetValueAtIndex(runA, runIndex);
            let runb = unsafeBitCast(run, to: CTRun.self)
            
            let CTFontName = unsafeBitCast(kCTFontAttributeName,
                                           to: UnsafeRawPointer.self)
            
            let runFontC = CFDictionaryGetValue(CTRunGetAttributes(runb),CTFontName)
            let runFontS = unsafeBitCast(runFontC, to: CTFont.self)
            
            let width = UIScreen.main.bounds.width
            
            var temp = 0
            var offset:CGFloat = 0.0
            
            for i in 0..<CTRunGetGlyphCount(runb) {
                let range = CFRangeMake(i, 1)
                let glyph = UnsafeMutablePointer<CGGlyph>.allocate(capacity: 1)
                glyph.initialize(to: 0)
                let position = UnsafeMutablePointer<CGPoint>.allocate(capacity: 1)
                position.initialize(to: .zero)
                CTRunGetGlyphs(runb, range, glyph)
                CTRunGetPositions(runb, range, position);
                
                let temp3 = CGFloat(position.pointee.x)
                let temp2 = (Int) (temp3 / width)
                let temp1 = 0
                if(temp2 > temp1){
                    
                    temp = temp2
                    offset = position.pointee.x - (CGFloat(temp) * width)
                }
                if let path = CTFontCreatePathForGlyph(runFontS,glyph.pointee,nil) {
                    let x = position.pointee.x - (CGFloat(temp) * width) - offset
                    let y = position.pointee.y - (CGFloat(temp) * 80)
                    let transform = CGAffineTransform(translationX: x, y: y)
                    paths.addPath(path, transform: transform)
                }
               
                glyph.deinitialize()
                glyph.deallocate(capacity: 1)
                position.deinitialize()
                position.deallocate(capacity: 1)
            }
        }
        
        let bezierPath = UIBezierPath()
        bezierPath.move(to: .zero)
        bezierPath.append(UIBezierPath(cgPath: paths))
        
        return bezierPath
    }
}

(2)使用样例(ViewController.swift
import UIKit

class ViewController: UIViewController {
    
    //文本输入框
    @IBOutlet weak var textField: UITextField!
    
    //文字笔迹书写组件
    var bezierText:BezierText!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //初始化文字笔迹书写组件
        bezierText = BezierText(frame: CGRect(x: 0, y: 160,
                        width: self.view.bounds.width, height: 50))
        self.view.addSubview(bezierText)
    }

    //书写按钮点击
    @IBAction func write(_ sender: Any) {
        bezierText.show(text: textField.text!)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

功能改进:在书写过程中增加只画笔

1,效果图

(1)在文字显示的过程中,会有一张钢笔图片随着轨迹的变化而移动,看起来文字像是由这只笔写出来的一样。
(2)文字写完后,钢笔又自动消失。
         

2,样例代码

这里主要是对 BezierText.swift 进行了一些修改,增加了钢笔图标的创建,及其相关动画的设置(高亮部分)。
import UIKit

class BezierText: UIView, CAAnimationDelegate {
    
    //字迹动画时间
    private let duration:TimeInterval = 3
    
    //字迹书写图层
    private let pathLayer = CAShapeLayer()
    
    //钢笔图标图层
    private var penLayer = CALayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        
        //初始化字迹图层
        pathLayer.frame = self.bounds
        pathLayer.isGeometryFlipped = true
        pathLayer.fillColor = UIColor.clear.cgColor
        pathLayer.lineWidth = 1
        pathLayer.strokeColor = UIColor.black.cgColor
        self.layer.addSublayer(pathLayer)
        
        //初始化钢笔图标图层
        let pen = UIImage(named: "pen")!
        penLayer.contents = pen.cgImage
        penLayer.anchorPoint = .zero
        penLayer.frame = CGRect(x: 0, y: 0, width: pen.size.width,
                                height: pen.size.height)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //动态书写指定文字
    func show(text: String) {
        //获取文字对应的贝塞尔曲线
        let textPath = bezierPathFrom(string: text)
        //让文字居中显示
        pathLayer.bounds = textPath.cgPath.boundingBox
        //设置笔记书写路径
        pathLayer.path = textPath.cgPath
        
        //添加笔迹书写动画
        let textAnimation = CABasicAnimation.init(keyPath: "strokeEnd")
        textAnimation.duration = duration
        textAnimation.fromValue = 0
        textAnimation.toValue = 1
        //textAnimation.repeatCount = HUGE
        pathLayer.add(textAnimation, forKey: "strokeEnd")
        
        //将钢笔图层添加到字迹图层中
        pathLayer.addSublayer(penLayer)
        
        //给钢笔图标添加移动动画
        let orbit = CAKeyframeAnimation(keyPath:"position")
        orbit.delegate = self
        orbit.duration = duration
        orbit.path = textPath.cgPath
        orbit.calculationMode = kCAAnimationPaced
        orbit.isRemovedOnCompletion = false
        orbit.fillMode = kCAFillModeForwards
        penLayer.add(orbit,forKey:"position")
    }
    
    //钢笔移动动画播放结束
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        //文字书写完毕后将钢笔移出
        penLayer.removeFromSuperlayer()
    }
    
    //将字符串转为贝塞尔曲线
    private func bezierPathFrom(string:String) -> UIBezierPath{
        let paths = CGMutablePath()
        let fontName = __CFStringMakeConstantString("SnellRoundhand")!
        let fontRef:AnyObject = CTFontCreateWithName(fontName, 20, nil)
        
        let attrString = NSAttributedString(string: string, attributes:
            [kCTFontAttributeName as NSAttributedStringKey : fontRef])
        let line = CTLineCreateWithAttributedString(attrString as CFAttributedString)
        let runA = CTLineGetGlyphRuns(line)
        
        for runIndex in 0..<CFArrayGetCount(runA) {
            let run = CFArrayGetValueAtIndex(runA, runIndex);
            let runb = unsafeBitCast(run, to: CTRun.self)
            
            let CTFontName = unsafeBitCast(kCTFontAttributeName,
                                           to: UnsafeRawPointer.self)
            
            let runFontC = CFDictionaryGetValue(CTRunGetAttributes(runb),CTFontName)
            let runFontS = unsafeBitCast(runFontC, to: CTFont.self)
            
            let width = UIScreen.main.bounds.width
            
            var temp = 0
            var offset:CGFloat = 0.0
            
            for i in 0..<CTRunGetGlyphCount(runb) {
                let range = CFRangeMake(i, 1)
                let glyph = UnsafeMutablePointer<CGGlyph>.allocate(capacity: 1)
                glyph.initialize(to: 0)
                let position = UnsafeMutablePointer<CGPoint>.allocate(capacity: 1)
                position.initialize(to: .zero)
                CTRunGetGlyphs(runb, range, glyph)
                CTRunGetPositions(runb, range, position);
                
                let temp3 = CGFloat(position.pointee.x)
                let temp2 = (Int) (temp3 / width)
                let temp1 = 0
                if(temp2 > temp1){
                    
                    temp = temp2
                    offset = position.pointee.x - (CGFloat(temp) * width)
                }
                if let path = CTFontCreatePathForGlyph(runFontS,glyph.pointee,nil) {
                    let x = position.pointee.x - (CGFloat(temp) * width) - offset
                    let y = position.pointee.y - (CGFloat(temp) * 80)
                    let transform = CGAffineTransform(translationX: x, y: y)
                    paths.addPath(path, transform: transform)
                }
               
                glyph.deinitialize()
                glyph.deallocate(capacity: 1)
                position.deinitialize()
                position.deallocate(capacity: 1)
            }
        }
        
        let bezierPath = UIBezierPath()
        bezierPath.move(to: .zero)
        bezierPath.append(UIBezierPath(cgPath: paths))
        
        return bezierPath
    }
}
源码下载hangge_1898.zip
评论0