对于有经验的开发者,你可能已使用过 Core Graphics API 来绘制形状与物件。这是一个非常强大的框架,可于建立向量图。在 SwiftUI 中,它也提供几个向量绘图 API,可供开发者绘制线条与形状。
在本章中,你将学习如何使用 Path
与内建的 Shape
(如 Circle
与 RoundedRectangle
), 来绘制线条、圆弧、圆饼图与环圈图。下列是我将要介绍的主题:
Shape
协议?如何遵守这个协议来绘制出自订的形状? 图8.1 列出了我们在后面的小节中所要建立的一些形状与图表。
在 SwiftUI 中,你可使用 Path 绘制线条与形状。如果你参考 Apple 的文件 (https://developer.apple.com/documentation/swiftui/path) , Path
是一个包含 2D 形状轮廓的结构,基本上,线条与形状是以路径逐步描绘。以图 8.2 为例,这是我们要在屏幕上绘制的矩形。
请叙说你要如何逐步绘制正方形呢?你可能会提供下列的描述:
这就是所谓的 Path
。如果将上面的步骤写成代码,代码如下所示:
Path() { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 300, y: 20))
path.addLine(to: CGPoint(x: 300, y: 200))
path.addLine(to: CGPoint(x: 20, y: 200))
}
.fill(.green)
这里初始化一个 Path
,并在闭包中提供详细的说明。你可以调用 move(to:)
方法移动至一个特定的坐标。要从目前的点画一条线到特定的点,则可以调用 addLine(to:)
方法。默认上,iOS 会以默认的前景色(即黑色)来填满路径,若填满其他颜色,则可以使用 .fill
修饰器,并设定为其颜色。
你可以使用 “App”模板建立一个新项目来测试代码。将项目命名为 SwiftUIShape
(或你喜欢的任何名称),然后在 body
输入上列的代码片段,预览画布即会显示出一个绿色矩形,如图 8.3 所示。
你不需要以颜色填满整个区域,如果你只想绘制线条的话,则可以使用 .stroke
修饰器,并指定线条的宽度与颜色,如图 8.4 所示。
因为我们没有指定将线条绘制到原点的步骤,所以显示为一个开放路径。要封闭路径的话,你可以在 Path
闭包的结尾处调用 closeSubpath()
方法,此方法会自动将目前点与起点连接起来。
Path
提供了多个内建的 API 来帮助你绘制不同的形状。你不只能够画出直线,还可以使用 addQuadCurve
、addCurve
与 addArc
方法来绘制出曲线与圆弧。例如:你想要在矩形顶部绘制出一个圆顶,如图 8.6 所示。
代码可以这样编写:
Path() { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addLine(to: CGPoint(x: 40, y: 60))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 0))
path.addLine(to: CGPoint(x: 230, y: 60))
path.addLine(to: CGPoint(x: 230, y: 100))
path.addLine(to: CGPoint(x: 20, y: 100))
}
.fill(Color.purple)
addQuadCurve
方法可以让你通过定义一个控制点(control point )来绘制曲线。参考图 8.6,(40, 60) 与(210, 60) 就是所谓的“锚点”(anchor point ),(125, 0) 则是计算建立圆顶形状的控制点,我不打算在这里讨论有关绘制曲线的数学,你可尝试修改控制点的值来查看效果。简单而言,该控制点控制如何绘制曲线。如果你将控制点放在更靠近矩形顶部的位置(例如:125, 30),则会绘制出不圆的外观。
如果要画出形状的边框,并同时以颜色填满形状,该怎么做呢? fill
与 stroke
修饰器无法并行使用,不过你可以使用 ZStack
来达到相同的效果,代码如下所示:
ZStack {
Path() { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addLine(to: CGPoint(x: 40, y: 60))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 0))
path.addLine(to: CGPoint(x: 230, y: 60))
path.addLine(to: CGPoint(x: 230, y: 100))
path.addLine(to: CGPoint(x: 20, y: 100))
}
.fill(Color.purple)
Path() { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addLine(to: CGPoint(x: 40, y: 60))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 0))
path.addLine(to: CGPoint(x: 230, y: 60))
path.addLine(to: CGPoint(x: 230, y: 100))
path.addLine(to: CGPoint(x: 20, y: 100))
path.closeSubpath()
}
.stroke(Color.black, lineWidth: 5)
}
我们使用相同的路径建立两个 Path
物件,然后使用 ZStack
来让一个 Path
物件叠在另一个 Path
物件上面。下面是使用 fill
填满紫色的圆顶矩形,并以黑色边框叠在上面,如图 8.7 所示。
SwiftUI 为开发者提供了一个方便的 API 来绘制圆弧,该 API 对于组合各种形状和物件(包含圆饼图)非常有用。要绘制圆弧,你可以撰写代码如下:
Path { path in
path.move(to: CGPoint(x: 200, y: 200))
path.addArc(center: .init(x: 200, y: 200), radius: 150, startAngle: .degrees(0), endAngle: .degrees(90), clockwise: true)
}
.fill(.green)
如果你将代码放入 body 中,则会在预览画布中看到一个填满绿色的圆弧,如图 8.8 所示。
在上列的代码中,我们先至起点 (200, 200),然后调用 addArc
来建立圆弧。addArc
方法接受几个参数:
如果只看“startAngle”与“endAngle”等两个参数的名称,应该会对其含义有点困惑, 图 8.9 可让你更加了解这些参数的含义。
使用 addArc
可轻松建立不同色扇形的圆饼图,你只需要以 ZStack
来重叠不同的扇形即可。组成其图的各个扇形都有不同 startAngle
值与 endAngle
值,下列是代码片段:
ZStack {
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(0), endAngle: .degrees(190), clockwise: true)
}
.fill(.yellow)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(190), endAngle: .degrees(110), clockwise: true)
}
.fill(.teal)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(110), endAngle: .degrees(90), clockwise: true)
}
.fill(.blue)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90), endAngle: .degrees(360), clockwise: true)
}
.fill(.purple)
}
这将渲染出一个具有四个扇形的圆饼图,如果你需要更多的扇形,则只要使用不同角度值来建立其他的路径物件即可。顺带一提,我使用的颜色是来自 iOS 所提供的标准颜色物件。你可以至下列的网址来了解完整的颜色物件:https://developer.apple.com/documentation/uikit/uicolor/standard_colors.
有时,你可能想从圆饼图切分出来,以突显特定的扇形。举例而言,要以紫色突显扇形时,你可以应用 offset
修饰器来改变扇形的位置:
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90), endAngle: .degrees(360), clockwise: true)
}
.fill(.purple)
.offset(x: 20, y: 20)
或者,你可以叠加一个边框来进一步吸引人们目光。如果你要在突显的扇形上加入标签,则可以叠上一个 Text
视图,如下所示:
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90), endAngle: .degrees(360), clockwise: true)
path.closeSubpath()
}
.stroke(Color(red: 52/255, green: 52/255, blue: 122/255), lineWidth: 10)
.offset(x: 20, y: 20)
.overlay(
Text("25%")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundColor(.white)
.offset(x: 80, y: -110)
)
该路径有与紫色扇形相同的起点角度与终点角度,但是它只仅绘制边框及加入一个文字视图,以使扇形突出,图 8.10 为最后的结果。
在我们深入了解 Shape
协议之前,我们先从一个简单的作业来开始。根据所学,使用 Path
绘制下列的形状,如图8.11 所示。
请先不要看解答,试着自己做看看。
好,要建立一个像这样的形状,你可使用 addLine
与 addQuadCurve
来建立一个 Path
:
Path() { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: 200, y: 0), control: CGPoint(x: 100, y: -20))
path.addLine(to: CGPoint(x: 200, y: 40))
path.addLine(to: CGPoint(x: 200, y: 40))
path.addLine(to: CGPoint(x: 0, y: 40))
}
.fill(Color.green)
如果你阅读过 Path
的文件,则可能找到另一个名为 addRect
的函数,该函数可以让你以特定的宽度与高度来绘制矩形。因此,下面是替代的解决方案:
Path() { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: 200, y: 0), control: CGPoint(x: 100, y: -20))
path.addRect(CGRect(x: 0, y: 0, width: 200, height: 40))
}
.fill(Color.green)
现在,我们来讨论一下 Shape
协议,这个协议非常简单,只有一个需求,当你使用它时,你必须实现下列函数:
func path(in rect: CGRect) -> Path
那么,我们何时需要使用 Shape
协议呢?试问你如何重新使用刚建立的 Path
呢?例如: 你想要建立一个圆顶(Dome)形状、大小弹性的按钮,该如何实现呢?
再看一下上列的代码,你以绝对坐标与尺寸来建立一个路径。为了建立相同但大小可变的形状,则可以建立一个结构来采用 Shape
协议,并实现 path(in:)
函数。当 path(in:)
函数被框架调用时,你将获得 rect
的大小,然后可在 rect
中绘制路径。
我们来了解如何建立圆顶形状,如此你便能更了解 Shape
协议。
struct Dome: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: rect.size.width, y: 0), control: CGPoint(x: rect.size.width/2, y: -(rect.size.width * 0.1)))
path.addRect(CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height))
return path
}
}
使用该协议后,我们会获得用于绘制路径的矩形区城,我们从 rect
可以找到矩形区域的宽度与高度来计算控制点,并绘制矩形底座。
藉由这个形状,你就可以使用它来建立各种 SwiftUI
控制组件。举例而言,你可以建立一个具有圆顶形状的按钮,如下所示:
Button(action: {
// 运行动作
}) {
Text("Test")
.font(.system(.title, design: .rounded))
.bold()
.foregroundColor(.white)
.frame(width: 250, height: 50)
.background(Dome().fill(Color.red))
}
我们将圆顶形状作为按钮的背景,其宽度与高度是基于指定的框架大小。
在前面,我们使用 Shape
协议自订了一个形状。而 SwiftUI
其实有几种内建形状,如圆形( Circle
)、矩形( Rectangle
)、圆角矩形( RoundedRectangle
)与椭圆( Ellipse
)等, 如果你不想要太花俏的话,这些形状已经足以建立一些常见的物件了。
举例而言,你要建立一个如图 8.13 所示的“停止”按钮,此按钮是由一个圆角矩形与一个圆形所组成,你可以撰写代码如下:
Circle()
.foregroundColor(.green)
.frame(width: 200, height: 200)
.overlay(
RoundedRectangle(cornerRadius: 5)
.frame(width: 80, height: 80)
.foregroundColor(.white)
)
这里,我们初始化一个 Circle
视图,然后将一个 RoundedRectangle
视图叠在上面。
通过内建形状的混搭,你可以为应用程序建立各种类型的向量式(vector-based )UI 控制组件。我再举另一个例子,图 8.14 为一个使用 Circle
建立的进度指示器。
这个进度指示器其实是由两个圆形所组成,下方是一个灰色圆环,而在灰色圆环的上方则是一个开口圆环,指示完成的进度。你可以在 ContentView
中撰写代码,如下所示:
struct ContentView: View {
private var purpleGradient = LinearGradient(gradient: Gradient(colors: [ Color(red: 207/255, green: 150/255, blue: 207/255), Color(red: 107/255, green: 116/255, blue: 179/255) ]), startPoint: .trailing, endPoint: .leading)
var body: some View {
ZStack {
Circle()
.stroke(Color(.systemGray6), lineWidth: 20)
.frame(width: 300, height: 300)
}
}
}
我们使用 stroke
修饰器来画出圆环的轮廓,若是你喜欢较粗(或较细)的线条,则可以调整 lineWidth
参数。而 purpleGradient
属性定义了紫色渐层,我们稍后在绘制开口圆环时会使用它。
现在,在 ZStack
中插入下列的代码,以建立开口圆环:
Circle()
.trim(from: 0, to: 0.85)
.stroke(purpleGradient, lineWidth: 20)
.frame(width: 300, height: 300)
.overlay {
VStack {
Text("85%")
.font(.system(size: 80, weight: .bold, design: .rounded))
.foregroundColor(.gray)
Text("Complete")
.font(.system(.body, design: .rounded))
.bold()
.foregroundColor(.gray)
}
}
建立一个开口圆环的技巧是加上一个 trim
修饰器。你可指定 from
值与 to
值,以指示要显示圆环的哪一个部分,在这个示例中,我们想要显示 85% 的进度,所以设定 from
的值为“0”、to 的值为“0.85”。
为了显示完成百分比( completion percentage),我们将一个文字视图叠在圆环的中间, 如图 8.16 所示。
最后要示范的是环圈图,如果你完全了解 trim
修饰器的用法,那么你可能已经知道我们将如何实现环圈图了。处理 trim
修饰器的值,我们可以将圆环切分成多段。
这是我们用来建立环圈图的技巧,代码如下所示:
ZStack {
Circle()
.trim(from: 0, to: 0.4)
.stroke(Color(.systemBlue), lineWidth: 80)
Circle()
.trim(from: 0.4, to: 0.6)
.stroke(Color(.systemTeal), lineWidth: 80)
Circle()
.trim(from: 0.6, to: 0.75)
.stroke(Color(.systemPurple), lineWidth: 80)
Circle()
.trim(from: 0.75, to: 1)
.stroke(Color(.systemYellow), lineWidth: 90)
.overlay(
Text("25%")
.font(.system(.title, design: .rounded))
.bold()
.foregroundColor(.white)
.offset(x: 80, y: -100)
)
}
.frame(width: 250, height: 250)
第一段圆弧只显示圆环的 40%,第二段圆弧显示圆环的 20%,不过请注意 from
值是“0.4”,而不是“0”,这可以让第二段圆弧连接第一段圆弧。
对于最后一个圆弧,我故意把线宽设得大一点,以使该段圆弧突出,如图8.17 所示。如果你不喜欢这样的设计,则可以将 linewidth
值由“90”改为“80”。
我希望你喜欢本章内容,并爱上示例项目。藉由框架所提供的绘图 API,你可以轻松为应用程序建立自订形状。Path 与 Shape 的运用还有很多,我仅介绍本章中的一些技巧,但请试着运用所学来施展一些魔法吧 !
在本章所准备的示例档中,有完整的项目可以下载: