iOS 贝塞尔曲线路径动画 SVG快速实现(Swift版)

本文将简单实现iOS快速路径绘制动画。

什么核心动画(Core Animation)、CAShapeLayer、UIBeizerPath等这些,这篇文章就不多说了。
反正看完这篇文章可以实现以下两种效果,虽然这两种效果很弱智,但高楼从地起嘛,懂了原理可以实现越来越复杂的动画组。Demo在最后可以下载,代码很烂,大神轻喷。



注:二维码效果来源:文章,因为原文里的部分功能无法重现,故在此重新写了个Demo。

案例一:快速实现路径动画
  • 步骤:

1.方案有很多,可以通过Sketch软件,先绘制出想要的图,然后导出SVG,再用PaintCode软件将SVG文件转换成所需的代码。这里我直接通过PaintCode绘制路径图,导出代码啦。
2.打开PaintCode,使用钢笔工具,随便画一条线,在软件的下方会直接生成贝塞尔曲线的代码(PS:也可以根据自己需要更改所使用的编程语言)。

3.复制生成的代码,去Xcode创建一个UIView。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ESSVGView: UIView {

override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
drawBezierPath()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func drawBezierPath() {

//通过PaintCode生成的代码
let pathPath = UIBezierPath()
pathPath.move(to: CGPoint(x: 179.5, y: 36.5))
pathPath.addCurve(to: CGPoint(x: 52.5, y: 118.5), controlPoint1: CGPoint(x: 95.5, y: 81.5), controlPoint2: CGPoint(x: 52.5, y: 118.5))
pathPath.addLine(to: CGPoint(x: 263.5, y: 118.5))
pathPath.addCurve(to: CGPoint(x: 75.5, y: 253.5), controlPoint1: CGPoint(x: 263.5, y: 118.5), controlPoint2: CGPoint(x: 4.5, y: 205.5))
pathPath.addCurve(to: CGPoint(x: 263.5, y: 253.5), controlPoint1: CGPoint(x: 146.5, y: 301.5), controlPoint2: CGPoint(x: 263.5, y: 253.5))
pathPath.addCurve(to: CGPoint(x: 75.5, y: 378.5), controlPoint1: CGPoint(x: 263.5, y: 253.5), controlPoint2: CGPoint(x: 222.5, y: 413.5))
pathPath.addCurve(to: CGPoint(x: 134.5, y: 527.5), controlPoint1: CGPoint(x: -71.5, y: 343.5), controlPoint2: CGPoint(x: 134.5, y: 527.5))
pathPath.addLine(to: CGPoint(x: 263.5, y: 404.5))

//添加path到ShapeLayer
let pathLayer = CAShapeLayer()
pathLayer.frame = frame
pathLayer.bounds = frame
pathLayer.isGeometryFlipped = false
pathLayer.path = pathPath.cgPath
pathLayer.strokeColor = UIColor.red.cgColor
pathLayer.fillColor = nil
pathLayer.lineWidth = 3
pathLayer.lineJoin = CAShapeLayerLineJoin.bevel
layer.addSublayer(pathLayer)

//添加动画
let animation = CABasicAnimation(keyPath: "strokeEnd") //strokeEnd正常绘制效果,strokeStart逐渐消失效果
animation.fromValue = 0 //初始值
animation.toValue = 1 //结束值
animation.repeatCount = 10 //重复次数
animation.duration = 10 //动画时间
pathLayer.add(animation, forKey: nil)

}
}

4.接着去ViewController,添加到视图里就OK了。简单粗暴!

1
2
3
4
5
6
7
8
9
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
let svg = ESSVGView(frame: view.frame)
view.addSubview(svg)
}

}
案例二:快速实现二维码动画
  • 步骤:
    1.首先找一个可以生成二维码并能导出SVG的网站,这里我用的是草料二维码生成,选择其他格式。


2.下载SVG格式文件

3.这里可以用文本编辑打开一下这个SVG文件,其实里面就是个XML,注意这些use就是二维码中一个个小方块。
每个方块x,y都知道了,width和height是固定值都是12。

4.接下来将SVG格式文件拖到工程,并用XML来解析这个文件

1
2
3
4
5
6
7
8
9
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
let qrPath = Bundle.main.path(forResource: "qr", ofType: "svg")!
let qrData = NSData(contentsOfFile: qrPath)
let xmlParser = XMLParser(data: qrData! as Data)
xmlParser.delegate = self
xmlParser.parse()
}

5.使用XML代理的2个方法进行处理

每当解析到一个新标签,这里就会被调用
1
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:])
整个 svg 文件解析完毕后,这里就会被调用
1
func parserDidEndDocument(_ parser: XMLParser)

6.最后我就直接把代码贴上来,关键地方都注释了干啥滴。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class ESXML: UIView {

var rects = [CGRect]()

override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
//解析SVG文件
let qrPath = Bundle.main.path(forResource: "qr", ofType: "svg")!
let qrData = NSData(contentsOfFile: qrPath)
let xmlParser = XMLParser(data: qrData! as Data)
xmlParser.delegate = self
xmlParser.parse()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

}

extension ESXML: XMLParserDelegate {

func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
if elementName == "use" {
//但elementName等于use的时候,记录rect并添加的rects数组中
let x = Double(attributeDict["x"]!)
let y = Double(attributeDict["y"]!)
let rect = CGRect(x: x!, y: y!, width: 12, height: 12)
rects.append(rect)
}else if elementName == "svg" {
//elementName等于svg的时候,记录这个二维码大小
let w = Double(attributeDict["width"]!)
let h = Double(attributeDict["height"]!)
bounds = CGRect(x: 0, y: 0, width: w!, height: h!)
}
}

func parserDidEndDocument(_ parser: XMLParser) {
//给layer添加阴影
layer.shadowColor = UIColor.gray.cgColor
layer.shadowRadius = 4
layer.shadowOpacity = 1
layer.shadowOffset = CGSize.zero

//遍历rects数组,把每一个rect变为一个贝塞尔路径,并用一个CAShapeLayer承载,最后添加动画。
for r in rects {
let rectLayer = CAShapeLayer()

rectLayer.fillColor = UIColor.orange.cgColor
rectLayer.strokeColor = nil
rectLayer.path = UIBezierPath(rect: CGRect(origin: CGPoint.zero, size: r.size)).cgPath
rectLayer.frame = r

var startTransform = CATransform3DIdentity
startTransform.m34 = 1.0 / -20 // 透视
startTransform = CATransform3DRotate(startTransform, CGFloat(Double.pi)*0.5, 0, 1, 0) // 沿 y 轴旋转 π/2 圈,待会再动画转回来

// transform 动画
let transAnim = CABasicAnimation(keyPath: "transform")
transAnim.duration = drand48() * 2 // 随机一个持续时间
transAnim.fromValue = NSValue(caTransform3D: startTransform)
transAnim.toValue = NSValue(caTransform3D: CATransform3DIdentity)
transAnim.repeatCount = 5
rectLayer.add(transAnim, forKey: "transAnim")

// 透明度动画
let alphaAnim = CABasicAnimation(keyPath: "opacity")
alphaAnim.duration = transAnim.duration
alphaAnim.fromValue = 0
alphaAnim.toValue = 1
alphaAnim.repeatCount = 5
rectLayer.add(alphaAnim, forKey: "alphaAnim")

layer.addSublayer(rectLayer)
}
}
}

Demo下载地址