Swift - 自动书写文字的动画效果(文字转贝塞尔曲线、实现字迹动画)
作者:hangge | 2018-01-08 08:10
有时一些提示性的文字(如: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
全部评论(1)
let attrString = NSAttributedString(string : string, attributes:
[kCTFontAttributeName as NSAttributedString : fontRef])
Cannot convert value of type 'CFString' to type 'NSAttributedString' in coercion
这句话报错,无法转换类型是为什么(新手见谅)
站长回复:我测试了下没问题啊,不知你Xcode是哪个版本的。