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
}
}
源码下载:
全部评论(1)
let attrString = NSAttributedString(string : string, attributes:
[kCTFontAttributeName as NSAttributedString : fontRef])
Cannot convert value of type 'CFString' to type 'NSAttributedString' in coercion
这句话报错,无法转换类型是为什么(新手见谅)
站长回复:我测试了下没问题啊,不知你Xcode是哪个版本的。