Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Drawing Paths and Shapes

Willie edited this page Feb 10, 2020 · 5 revisions

绘制路径和形状

用户访问列表中的地标时会获得徽章,为此我们需要创建徽章。在本文中,我们将通过组合路径和形状来创建徽章,然后把它和另一个表示位置的形状叠在一起。

我们可以尝试使用叠加符号来给不同类型的地标创建多个徽章,修改它的重复次数或更改各种角度和比例。

下载项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。

  • 预计完成时间:25 分钟
  • 项目文件:下载

1. 创建一个徽章视图

首先我们创建一个使用 SwiftUI 中矢量绘图 API 的徽章视图。

1.1 选择 File > New > File ,从 iOS Templates 中选择 SwiftUI View 。单击 Next ,命名为 Badge ,单击 Create

1.2 在定义徽章的视图前,先让徽章视图显示 Badge

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 Text("Badge")
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

2. 绘制徽章背景

SwiftUI 中的图形 API 绘制自定义的徽章形状。

2.1 查看 HexagonParameters.swift 文件中的代码。

HexagonParameters 结构体定义了绘制徽章的六边形 shape 的细节,我们不用修改这些数据,直接使用它们来指定绘制徽章的线条和曲线的控制点。

HexagonParameters.swift

import SwiftUI
struct HexagonParameters {
 struct Segment {
 let useWidth: (CGFloat, CGFloat, CGFloat)
 let xFactors: (CGFloat, CGFloat, CGFloat)
 let useHeight: (CGFloat, CGFloat, CGFloat)
 let yFactors: (CGFloat, CGFloat, CGFloat)
 }
 
 static let adjustment: CGFloat = 0.085
 
 static let points = [
 Segment(
 useWidth: (1.00, 1.00, 1.00),
 xFactors: (0.60, 0.40, 0.50),
 useHeight: (1.00, 1.00, 0.00),
 yFactors: (0.05, 0.05, 0.00)
 ),
 Segment(
 useWidth: (1.00, 1.00, 0.00),
 xFactors: (0.05, 0.00, 0.00),
 useHeight: (1.00, 1.00, 1.00),
 yFactors: (0.20 + adjustment, 0.30 + adjustment, 0.25 + adjustment)
 ),
 Segment(
 useWidth: (1.00, 1.00, 0.00),
 xFactors: (0.00, 0.05, 0.00),
 useHeight: (1.00, 1.00, 1.00),
 yFactors: (0.70 - adjustment, 0.80 - adjustment, 0.75 - adjustment)
 ),
 Segment(
 useWidth: (1.00, 1.00, 1.00),
 xFactors: (0.40, 0.60, 0.50),
 useHeight: (1.00, 1.00, 1.00),
 yFactors: (0.95, 0.95, 1.00)
 ),
 Segment(
 useWidth: (1.00, 1.00, 1.00),
 xFactors: (0.95, 1.00, 1.00),
 useHeight: (1.00, 1.00, 1.00),
 yFactors: (0.80 - adjustment, 0.70 - adjustment, 0.75 - adjustment)
 ),
 Segment(
 useWidth: (1.00, 1.00, 1.00),
 xFactors: (1.00, 0.95, 1.00),
 useHeight: (1.00, 1.00, 1.00),
 yFactors: (0.30 + adjustment, 0.20 + adjustment, 0.25 + adjustment)
 )
 ]
}

2.2 在 Badge.swift 中,给徽章添加一个路径形状,然后调用 fill() 修饰符把形状转换成一个视图。

我们可以使用路径组合直线、曲线和其他绘图单元来形成更复杂的形状,比如这里徽章的六边形背景。

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 //
 Path { path in
 
 }
 .fill(Color.black)
 //
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

2.3 给路径添加起始点。

move(to:) 方法把绘制光标移动到一个形状的边上,就像钢笔或铅笔悬停在该位置,等待开始绘制。

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 Path { path in
 //
 var width: CGFloat = 100.0
 let height = width
 path.move(to: CGPoint(x: width * 0.95, y: height * 0.20))
 //
 }
 .fill(Color.black)
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

2.4 绘制形状数据中的每一个点,创建一个大致的六边形形状。

addLine(to:) 拿到一点并绘制出来。连续调用 addLine(to:) 方法,在每两个点之间画一条线。

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 Path { path in
 var width: CGFloat = 100.0
 let height = width
 path.move(to: CGPoint(x: width * 0.95, y: height * 0.20))
 
 //
 HexagonParameters.points.forEach {
 path.addLine(
 to: .init(
 x: width * 0ドル.useWidth.0 * 0ドル.xFactors.0,
 y: height * 0ドル.useHeight.0 * 0ドル.yFactors.0
 )
 )
 }
 //
 }
 .fill(Color.black)
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

现在我们的六角形看起来不对劲,但这是正常的。在接下来的几个步骤中,我们会让六边形看起来更像本开文头所示的徽章形状。

2.5 使用 addQuadCurve(to:control:) 方法来给徽章的角绘制贝塞尔曲线。

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 Path { path in
 var width: CGFloat = 100.0
 let height = width
 //
 path.move(
 to: CGPoint(
 x: width * 0.95,
 y: height * (0.20 + HexagonParameters.adjustment)
 )
 )
 //
 
 HexagonParameters.points.forEach {
 path.addLine(
 to: .init(
 x: width * 0ドル.useWidth.0 * 0ドル.xFactors.0,
 y: height * 0ドル.useHeight.0 * 0ドル.yFactors.0
 )
 )
 
 //
 path.addQuadCurve(
 to: .init(
 x: width * 0ドル.useWidth.1 * 0ドル.xFactors.1,
 y: height * 0ドル.useHeight.1 * 0ドル.yFactors.1
 ),
 control: .init(
 x: width * 0ドル.useWidth.2 * 0ドル.xFactors.2,
 y: height * 0ドル.useHeight.2 * 0ドル.yFactors.2
 )
 )
 //
 }
 }
 .fill(Color.black)
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

2.6 把徽章的形状包装在一个 GeometryReader 中,这样徽章就不会用硬编码的大小(100)而是使用其所包含视图的大小。

当包含徽章的视图不是正方形时,使用最小的几何二维可以保留徽章的长宽比。

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 //
 GeometryReader { geometry in
 Path { path in
 var width: CGFloat = min(geometry.size.width, geometry.size.height)
 let height = width
 path.move(
 to: CGPoint(
 x: width * 0.95,
 y: height * (0.20 + HexagonParameters.adjustment)
 )
 )
 
 HexagonParameters.points.forEach {
 path.addLine(
 to: .init(
 x: width * 0ドル.useWidth.0 * 0ドル.xFactors.0,
 y: height * 0ドル.useHeight.0 * 0ドル.yFactors.0
 )
 )
 
 path.addQuadCurve(
 to: .init(
 x: width * 0ドル.useWidth.1 * 0ドル.xFactors.1,
 y: height * 0ドル.useHeight.1 * 0ドル.yFactors.1
 ),
 control: .init(
 x: width * 0ドル.useWidth.2 * 0ドル.xFactors.2,
 y: height * 0ドル.useHeight.2 * 0ドル.yFactors.2
 )
 )
 }
 }
 .fill(Color.black)
 }
 //
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

2.7 使用 xScalexOffset 调整变量将徽章置于其几何体中心。

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 GeometryReader { geometry in
 Path { path in
 var width: CGFloat = min(geometry.size.width, geometry.size.height)
 let height = width
 //
 let xScale: CGFloat = 0.832
 let xOffset = (width * (1.0 - xScale)) / 2.0
 width *= xScale
 //
 path.move(
 to: CGPoint(
 //
 x: xOffset + width * 0.95,
 //
 y: height * (0.20 + HexagonParameters.adjustment)
 )
 )
 
 HexagonParameters.points.forEach {
 path.addLine(
 to: .init(
 //
 x: xOffset + width * 0ドル.useWidth.0 * 0ドル.xFactors.0,
 //
 y: height * 0ドル.useHeight.0 * 0ドル.yFactors.0
 )
 )
 
 path.addQuadCurve(
 to: .init(
 //
 x: xOffset + width * 0ドル.useWidth.1 * 0ドル.xFactors.1,
 //
 y: height * 0ドル.useHeight.1 * 0ドル.yFactors.1
 ),
 control: .init(
 //
 x: xOffset + width * 0ドル.useWidth.2 * 0ドル.xFactors.2,
 //
 y: height * 0ドル.useHeight.2 * 0ドル.yFactors.2
 )
 )
 }
 }
 .fill(Color.black)
 }
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

2.8 参照设计,把徽章背景的纯黑色改成渐变色。

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 GeometryReader { geometry in
 Path { path in
 var width: CGFloat = min(geometry.size.width, geometry.size.height)
 let height = width
 let xScale: CGFloat = 0.832
 let xOffset = (width * (1.0 - xScale)) / 2.0
 width *= xScale
 path.move(
 to: CGPoint(
 x: xOffset + width * 0.95,
 y: height * (0.20 + HexagonParameters.adjustment)
 )
 )
 
 HexagonParameters.points.forEach {
 path.addLine(
 to: .init(
 x: xOffset + width * 0ドル.useWidth.0 * 0ドル.xFactors.0,
 y: height * 0ドル.useHeight.0 * 0ドル.yFactors.0
 )
 )
 
 path.addQuadCurve(
 to: .init(
 x: xOffset + width * 0ドル.useWidth.1 * 0ドル.xFactors.1,
 y: height * 0ドル.useHeight.1 * 0ドル.yFactors.1
 ),
 control: .init(
 x: xOffset + width * 0ドル.useWidth.2 * 0ドル.xFactors.2,
 y: height * 0ドル.useHeight.2 * 0ドル.yFactors.2
 )
 )
 }
 }
 //
 .fill(LinearGradient(
 gradient: .init(colors: [Self.gradientStart, Self.gradientEnd]),
 startPoint: .init(x: 0.5, y: 0),
 endPoint: .init(x: 0.5, y: 0.6)
 ))
 //
 }
 }
 //
 static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
 static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
 //
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

2.9 把 aspectRatio(_:contentMode:) 修饰符添加到渐变的填充上。

即使徽章的父项不是正方形,也可以通过保持1:1的宽高比,让徽章处于视图中心的位置。

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 GeometryReader { geometry in
 Path { path in
 var width: CGFloat = min(geometry.size.width, geometry.size.height)
 let height = width
 let xScale: CGFloat = 0.832
 let xOffset = (width * (1.0 - xScale)) / 2.0
 width *= xScale
 path.move(
 to: CGPoint(
 x: xOffset + width * 0.95,
 y: height * (0.20 + HexagonParameters.adjustment)
 )
 )
 
 HexagonParameters.points.forEach {
 path.addLine(
 to: .init(
 x: xOffset + width * 0ドル.useWidth.0 * 0ドル.xFactors.0,
 y: height * 0ドル.useHeight.0 * 0ドル.yFactors.0
 )
 )
 
 path.addQuadCurve(
 to: .init(
 x: xOffset + width * 0ドル.useWidth.1 * 0ドル.xFactors.1,
 y: height * 0ドル.useHeight.1 * 0ドル.yFactors.1
 ),
 control: .init(
 x: xOffset + width * 0ドル.useWidth.2 * 0ドル.xFactors.2,
 y: height * 0ドル.useHeight.2 * 0ドル.yFactors.2
 )
 )
 }
 }
 .fill(LinearGradient(
 gradient: .init(colors: [Self.gradientStart, Self.gradientEnd]),
 startPoint: .init(x: 0.5, y: 0),
 endPoint: .init(x: 0.5, y: 0.6)
 ))
 //
 .aspectRatio(1, contentMode: .fit)
 //
 }
 }
 static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
 static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

3. 绘制徽章符号

地标徽章的中心有一个自定义标志,它由 Landmarks app icon 中的山峰转变而来。

山峰由两个形状组成:一个代表峰顶的雪盖,另一个代表沿途的植被。我们使用两个三角形的形状绘制它们,然后由一个小间隙分开。

3.1 创建一个名为 BadgeBackground.swift 的新文件,将徽章视图的主体封装为新文件中的 BadgeBackground 视图,作为为其他视图创建 Badge 视图的一部分。

BadgeBackground.swift

import SwiftUI
struct BadgeBackground: View {
 var body: some View {
 GeometryReader { geometry in
 Path { path in
 var width: CGFloat = min(geometry.size.width, geometry.size.height)
 let height = width
 let xScale: CGFloat = 0.832
 let xOffset = (width * (1.0 - xScale)) / 2.0
 width *= xScale
 path.move(
 to: CGPoint(
 x: xOffset + width * 0.95,
 y: height * (0.20 + HexagonParameters.adjustment)
 )
 )
 
 HexagonParameters.points.forEach {
 path.addLine(
 to: .init(
 x: xOffset + width * 0ドル.useWidth.0 * 0ドル.xFactors.0,
 y: height * 0ドル.useHeight.0 * 0ドル.yFactors.0
 )
 )
 
 path.addQuadCurve(
 to: .init(
 x: xOffset + width * 0ドル.useWidth.1 * 0ドル.xFactors.1,
 y: height * 0ドル.useHeight.1 * 0ドル.yFactors.1
 ),
 control: .init(
 x: xOffset + width * 0ドル.useWidth.2 * 0ドル.xFactors.2,
 y: height * 0ドル.useHeight.2 * 0ドル.yFactors.2
 )
 )
 }
 }
 .fill(LinearGradient(
 gradient: .init(colors: [Self.gradientStart, Self.gradientEnd]),
 startPoint: .init(x: 0.5, y: 0),
 endPoint: .init(x: 0.5, y: 0.6)
 ))
 .aspectRatio(1, contentMode: .fit)
 }
 }
 static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
 static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
}
struct BadgeBackground_Previews: PreviewProvider {
 static var previews: some View {
 BadgeBackground()
 }
}

3.2 将 BadgeBackground 放置在徽章的正文中来恢复徽章。

Badge.swift

import SwiftUI
struct Badge: View {
 var body: some View {
 BadgeBackground()
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

3.3 给设计中旋转样式的山峰形状创建一个新的自定义视图 BadgeSymbol

BadgeSymbol.swift

import SwiftUI
struct BadgeSymbol: View {
 var body: some View {
 Text("Badge Symbol")
 }
}
struct BadgeSymbol_Previews: PreviewProvider {
 static var previews: some View {
 BadgeSymbol()
 }
}

3.4 使用路径 API 绘制符号的顶部。

试着调整一下与 spacingtopWidthtopHeight 常量关联的系数,体验它们是如何影响整体形状的。

BadgeSymbol.swift

import SwiftUI
struct BadgeSymbol: View {
 var body: some View {
 //
 GeometryReader { geometry in
 Path { path in
 let width = min(geometry.size.width, geometry.size.height)
 let height = width * 0.75
 let spacing = width * 0.030
 let middle = width / 2
 let topWidth = 0.226 * width
 let topHeight = 0.488 * height
 
 path.addLines([
 CGPoint(x: middle, y: spacing),
 CGPoint(x: middle - topWidth, y: topHeight - spacing),
 CGPoint(x: middle, y: topHeight / 2 + spacing),
 CGPoint(x: middle + topWidth, y: topHeight - spacing),
 CGPoint(x: middle, y: spacing)
 ])
 }
 }
 //
 }
}
struct BadgeSymbol_Previews: PreviewProvider {
 static var previews: some View {
 BadgeSymbol()
 }
}

3.5 绘制符号的底部。

使用 move(to:) 修饰符在同一路径中的多个形状之间插入间隙。

BadgeSymbol.swift

import SwiftUI
struct BadgeSymbol: View {
 var body: some View {
 GeometryReader { geometry in
 Path { path in
 let width = min(geometry.size.width, geometry.size.height)
 let height = width * 0.75
 let spacing = width * 0.030
 let middle = width / 2
 let topWidth = 0.226 * width
 let topHeight = 0.488 * height
 
 path.addLines([
 CGPoint(x: middle, y: spacing),
 CGPoint(x: middle - topWidth, y: topHeight - spacing),
 CGPoint(x: middle, y: topHeight / 2 + spacing),
 CGPoint(x: middle + topWidth, y: topHeight - spacing),
 CGPoint(x: middle, y: spacing)
 ])
 
 //
 path.move(to: CGPoint(x: middle, y: topHeight / 2 + spacing * 3))
 path.addLines([
 CGPoint(x: middle - topWidth, y: topHeight + spacing),
 CGPoint(x: spacing, y: height - spacing),
 CGPoint(x: width - spacing, y: height - spacing),
 CGPoint(x: middle + topWidth, y: topHeight + spacing),
 CGPoint(x: middle, y: topHeight / 2 + spacing * 3)
 ])
 //
 }
 }
 }
}
struct BadgeSymbol_Previews: PreviewProvider {
 static var previews: some View {
 BadgeSymbol()
 }
}

3.6 按照设计,给符号填充颜色。

BadgeSymbol.swift

import SwiftUI
struct BadgeSymbol: View {
 //
 static let symbolColor = Color(red: 79.0 / 255, green: 79.0 / 255, blue: 191.0 / 255)
 //
 var body: some View {
 GeometryReader { geometry in
 Path { path in
 let width = min(geometry.size.width, geometry.size.height)
 let height = width * 0.75
 let spacing = width * 0.030
 let middle = width / 2
 let topWidth = 0.226 * width
 let topHeight = 0.488 * height
 
 path.addLines([
 CGPoint(x: middle, y: spacing),
 CGPoint(x: middle - topWidth, y: topHeight - spacing),
 CGPoint(x: middle, y: topHeight / 2 + spacing),
 CGPoint(x: middle + topWidth, y: topHeight - spacing),
 CGPoint(x: middle, y: spacing)
 ])
 
 path.move(to: CGPoint(x: middle, y: topHeight / 2 + spacing * 3))
 path.addLines([
 CGPoint(x: middle - topWidth, y: topHeight + spacing),
 CGPoint(x: spacing, y: height - spacing),
 CGPoint(x: width - spacing, y: height - spacing),
 CGPoint(x: middle + topWidth, y: topHeight + spacing),
 CGPoint(x: middle, y: topHeight / 2 + spacing * 3)
 ])
 }
 //
 .fill(Self.symbolColor)
 //
 }
 }
}
struct BadgeSymbol_Previews: PreviewProvider {
 static var previews: some View {
 BadgeSymbol()
 }
}

4. 组合徽章的前景和背景

设计中要求在徽章的背景上旋转并重复多次山峰的形状。

我们来定义一个新的旋转类型,并利用 ForEach 视图让山峰形状的多个副本保持相同的设置 。

4.1 创建一个新的 RotatedBadgeSymbol 视图来封装旋转的符号。

试着在预览中调整角度来测试旋转的效果。

RotatedBadgeSymbol.swift

import SwiftUI
struct RotatedBadgeSymbol: View {
 let angle: Angle
 
 var body: some View {
 BadgeSymbol()
 .padding(-60)
 .rotationEffect(angle, anchor: .bottom)
 }
}
struct RotatedBadgeSymbol_Previews: PreviewProvider {
 static var previews: some View {
 RotatedBadgeSymbol(angle: .init(degrees: 5))
 }
}

4.2 在 Badge.swift 中,用一个 ZStack 把徽章的 symbol 叠加在徽章的背景上。

Badge.swift

import SwiftUI
struct Badge: View {
 //
 var badgeSymbols: some View {
 RotatedBadgeSymbol(angle: .init(degrees: 0))
 .opacity(0.5)
 }
 //
 
 var body: some View {
 //
 ZStack {
 BadgeBackground()
 
 self.badgeSymbols
 }
 //
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

现在徽章符号与预期的设计相比,它与背景的比例太大。

4.3 通过读取周围的几何图形并缩放符号来修改徽章符号的大小。

Badge.swift

import SwiftUI
struct Badge: View {
 var badgeSymbols: some View {
 RotatedBadgeSymbol(angle: .init(degrees: 0))
 .opacity(0.5)
 }
 
 var body: some View {
 ZStack {
 BadgeBackground()
 
 //
 GeometryReader { geometry in
 self.badgeSymbols
 .scaleEffect(1.0 / 4.0, anchor: .top)
 .position(x: geometry.size.width / 2.0, y: (3.0 / 4.0) * geometry.size.height)
 }
 //
 }
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

4.4 添加 ForEach 视图来旋转并显示徽章符号的副本。

完整的 360° 旋转分为八个部分,通过重复山峰符号来创建一个类似太阳的样式。

Badge.swift

import SwiftUI
struct Badge: View {
 //
 static let rotationCount = 8
 //
 
 var badgeSymbols: some View {
 //
 ForEach(0..<Badge.rotationCount) { i in
 RotatedBadgeSymbol(
 angle: .degrees(Double(i) / Double(Badge.rotationCount)) * 360.0
 )
 }
 .opacity(0.5)
 //
 }
 
 var body: some View {
 ZStack {
 BadgeBackground()
 
 GeometryReader { geometry in
 self.badgeSymbols
 .scaleEffect(1.0 / 4.0, anchor: .top)
 .position(x: geometry.size.width / 2.0, y: (3.0 / 4.0) * geometry.size.height)
 }
 }
 //
 .scaledToFit()
 //
 }
}
struct Badge_Previews: PreviewProvider {
 static var previews: some View {
 Badge()
 }
}

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /