其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记
形状
我们使用过的所有视图都是容器或设计用于在屏幕上展示预定义内容,但SwiftUI还内置了图形视图可创建自定义控件或用作闭包。这些视图与之前介绍过的类似,可使用大部分修饰符,但经过特别设计在屏幕上画自定义图形。
通用形状
SwiftUI允许我们创建预定义或自定义开关。以下是用于创建标准开关的视图。
- Rectangle():这个初始化方法创建一个
Rectangle
视图。矩形的尺寸由视图的框架决定。 - RoundedRectangle(cornerRadius: CGFloat, style: RoundedCornerStyle):此初始化方法创建一个
RoundedRectangle
视图。cornerRadius
参数指定圆角的半径,style
参数是RoundedCornerStyle
类型的枚举,指定所用曲率的类型。值有circular
和continuous
。该视图还包包通过CGSize
的值定义半径的初始化方法:RoundedRectangle(cornerSize: CGSize, style: RoundedCornerStyle)
。 - Circle():些初始化方法创建一个
Circle
视图。圆的直径由视图的边框决定。 - Ellipse():此初始化方法创建一个
Ellipse
视图。椭圆的大小由视图框架的宽和高决定。 - Capsule(style: RoundedCornerStyle):此初始化方法创建一个
Capsule
视图。style
参数是指定应用于边角曲率类型的枚举。值有circular
和continuous
。
和其它视图一样,图形视图在未指定大小时采用其容器的大小,但可以通过frame()
修饰符声明具体的大小。以下示例展示了所有的标准形状。我们将视图放到了水平的ScrollView
视图中,以例可以滚动列表。
示例11-1:绘制标准图形
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
struct ContentView: View { var body: some View { VStack { ScrollView(.horizontal, showsIndicators: true) { HStack { Rectangle() .frame(width: 100, height: 100) RoundedRectangle(cornerRadius: 25, style: .continuous) .frame(width: 100, height: 100) Circle() .frame(width: 100, height: 100) Ellipse() .frame(width: 100, height: 50) Capsule() .frame(width: 100, height: 50) }.padding() } Spacer() } } } |
图11-1:标准形状
✍️跟我一起做:创建一个多平台项目。使用示例11-1中的代码更新ContentView
视图。我们在屏幕上看不全所有形状,将视图向左滚动。使用这个项目测试本章后续的示例。
默认,视图通过依赖于外观模式的颜色进行渲染(浅色模式为黑色,深色模式为白色),但我们可以通过如下的修饰符对形状进行填充和边框设置。
- fill(View):此修饰符通过参数指定的视图填充形状。参数是一个表示颜色、渐变或图片的视图。
- stroke(View, lineWidth: CGFloat):此修饰符定义形状的边框。第一个参数是表示颜色、渐变或图片的视图,
lineWidth
参数定义边框的宽度。 - stroke(View, style: StrokeStyle):此修饰符定义形状的边框。第一个参数是表示颜色、渐变或图片的视图,
style
参数为StrokeStyle
类型的结构体,定义边框的宽度、末端、连接、转角限量、虚线和虚线相位。 - strokeBorder(View, lineWidth: CGFloat):这一修饰符定义开关的内边框。第一个参数是表示颜色、渐变或图片的视图,
lineWidth
参数定义边框的宽度。 - strokeBorder(View, style: StrokeStyle):此修饰符定义形状的内边框。第一个参数是表示颜色、渐变或图片的视图,
style
参数为StrokeStyle
类型的结构体,定义边框的宽度、末端、连接、转角限量、虚线和虚线相位。
通过这些修饰符我们可以改变的有两类:填充和边框。填充由fill()
修饰符以及表示内容的视图定义,如Color
视图。
示例11-2:使用颜色填充形状
1 2 3 4 5 6 7 |
struct ContentView: View { var body: some View { RoundedRectangle(cornerRadius: 25) .fill(Color.red) .frame(width: 100, height: 100) } } |
注意fill()
修饰符由RoundedRectangle
视图实现,但frame()
返回另一个视图。因此 任意定义形状的修饰符,比如fill()
,必须在其它修饰符之前,如frame()
。本例中,我们使用这些修饰符创建一个带圆角的红色方框。
图11-2:矩形
添加边框的流程相似,但两种类型的修饰符结果稍有不同。stroke()
修饰符向内及向外扩展边框,而strokeBorder()
修饰符创建一个内边框。
示例11-3:定义边框
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct ContentView: View { var body: some View { HStack { RoundedRectangle(cornerRadius: 25) .stroke(Color.red, lineWidth: 20) .frame(width: 100, height: 100) .padding() RoundedRectangle(cornerRadius: 25) .strokeBorder(Color.red, lineWidth: 20) .frame(width: 100, height: 100) .padding() } } } |
以上视图中包含两个边框为20点的RoundedRectangle
视图,但因为使用的是不同的修饰符,边框不同。第一个方框的边框有一差别在图形外绘制,而另一半在视图边框内绘制,但第二个方框的边框在边框内,如下图所示。
图11-3:带不同边框的矩形
这两个修饰符也可以接收StrokeStyle
结构体对边框进行调优。该结构体提供了如下的初始化方法。
- StrokeStyle(lineWidth: CGFloat, lineCap: CGLineCap, lineJoin: CGLineJoin, miterLimit: CGFloat, dash: [CGFloat], dashPhase: CGFloat):此初始化方法创建一个配置笔触的
StrokeStyle
结构体。lineWidth
参数指定宽度。lineCap
参数指定线末端的样式。它是一个枚举,值有butt
(平直方形)、round
(圆形)和square
(方形)。lineJoin
参数设置两个连接线结点的样式。它是一个枚举,值有miter
(锐利末端)、round
(圆形末端)和bevel
(方形末端)。miterLimit
参数在lineJoin
参数为miter
时指定线扩展多长。dash
参数指定虚线笔触每段的长度。dashPhase
参数指定虚线的起点。
注:butt
和square
都是方形末端,但前者没有扩展段,后者有扩展段。
下例创建了一个RoundedRectangle
视图,边界以15点宽和圆形末端配置虚线。
示例11-4:定义自定义边框
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { let lineStyle = StrokeStyle(lineWidth: 15, lineCap: .round, lineJoin: .round, miterLimit: 0, dash: [20], dashPhase: 0) var body: some View { RoundedRectangle(cornerRadius: 25) .stroke(Color.red, style: lineStyle) .frame(width: 100, height: 100) } } |
图11-4:带自定义笔触的矩形
形状也是视图,因此可以结合SwiftUI的视图和控件使用。比如下例中将Capsule
形状赋值为按钮的背景。
示例11-5:结合形状和其它视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
struct ContentView: View { @State private var setActive: Bool = true var body: some View { VStack { Button(action: { setActive.toggle() }, label: { Text(setActive ? "Active" : "Inactive") .font(.title) .foregroundColor(Color.white) .padding(.horizontal, 30) .padding(.vertical, 10) }) .background( Capsule() .fill(setActive ? Color.green : Color.red) ) Spacer() }.padding() } } |
事实上,background()
修饰符有一个版本专门用于形状。
- background(Color, in: Shape):这一修饰符将形状赋值给视图的背景。第一个参数指定颜色,
in
参数指定形状。
使用该修饰符,我们可以通过一行代码声明上例中的按钮。
示例11-6:将形状赋值为按钮的背景
1 |
.background(setActive ? Color.green : Color.red, in: Capsule()) |
该按钮切换@State
属性的值。若值为true
,展示标签Active并将绿色胶囊赋值为按钮的背景,否则显示标签Inactive并将胶囊变为红色。
图11-5:图形按钮
渐变
形状的填充和边框也可以使用颜色渐变定义。SwiftUI有4个显示渐变的结构体:LinearGradient
、RadialGradient
、AngularGradient
和EllipticalGradient
。这些结构体遵循ShapeStyle
协议,协议定义了如下方法创建自定义实例。
- linearGradient(Gradient, startPoint: UnitPoint, endPoint: UnitPoint):此方法返回一个线性渐变。
gradient
参数是供使用的颜色渐变,startPoint
和endPoint
参数指定渐变在形状中开始和结束的点。 - radialGradient(Gradient, center: UnitPoint, startRadius: CGFloat, endRadius: CGFloat):此方法返回一个环形渐变。
gradient
参数是供使用的颜色渐变。center
参数指定圆心的位置,startRadius
和endRadius
参数指定渐变的起始和结束位置。 - ellipticalGradient(Gradient, center: UnitPoint, startRadiusFraction: CGFloat, endRadiusFraction: CGFloat):此方法返回一个椭圆形状的径向渐变。
gradient
参数是供使用的颜色渐变。center
参数指定椭圆的圆心,startRadiusFraction
和endRadiusFraction
指定椭圆的半径。 - angularGradient(Gradient, center: UnitPoint, startAngle: Angle, endAngle: Angle):此方法返回一个角度渐变。
gradient
参数是供使用的颜色渐变。center
参数指定形状的中心,startAngle
参数指定渐变的起始角度,endAngle
指定结束角度。 - conicGradient(Gradient, center: UnitPoint, angle: Angle):此方法返回一个锥形渐变。
gradient
参数是供使用的颜色渐变。center
参数指定圆锥顶点的位置,angle
参数指定渐变起始的角度。
这些返回前面介绍的渐变结构体中一种的实例,但颜色的渐变通过Gradient
结构体来定义。
- Gradient(colors: [Color]):此初始化方法通过参数指定的颜色创建一个渐变。
colors
参数是一个Color
视图数组。 - Gradient(stops: [Gradient.Stop]):此初始化方法通过参数指定的颜色创建一个渐变。
stops
参数为指定颜色的Stop
结构体的数组,以及结束的时机。
另一个展示渐变所需的值是UnitPoint
结构体。它类似CGPoint
结构体,但专门设计用于处理图形结构体。
- UnitPoint(x: CGFloat, y: CGFloat):此初始化方法创建一个
UnitPoint
结构体。x
和y
参数指定该点的x和y坐标。对于渐变,这些参数值在0.0到1.0之间。
UnitPoint
结构体包含定义通用点的类型属性bottom
、bottomLeading
、bottomTrailing
、center
、leading
、top
、topLeading
、topTrailing
、trailing
和zero
。例如,我们对线性渐变应用bottom
和top
来从形状的底部到顶部绘制渐变。
示例11-7:定义线性渐变
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { let gradient = Gradient(colors: [Color.red, Color.green]) var body: some View { RoundedRectangle(cornerRadius: 25) .fill(.linearGradient(gradient, startPoint: .bottom, endPoint: .top)) .frame(width: 100, height: 100) } } |
示例11-7中的代码定义了两种颜色,红色和绿色的渐变,然后使用linearGradient()
方法返回的结构体将渐变应用于RoundedRectangle
视图。因为使用bottom
声明了起始点,并用top
声明了结束点,按从底到顶的顺序显示Gradient
结构体中声明的颜色。
图11-6:线性渐变
创建渐变时若未指定结束颜色,颜色会在渐变占有的整个区域中均匀分布。如果希望调整分布,必须通过Stop
结构体定义颜色。
- Stop(color: Color, location: CGFloat):此初始化方法通过结束值创建一个颜色。
color
参数指定颜色,location
参数指定颜色在渐变中开始的位置(值为0.0到1.0)。
下例重现了前面的案例,但这次绿色从0.4处开始(渐变占据区域的40%)。
示例11-8:通过自定义了结束点定义线性渐变
1 2 3 4 5 6 7 8 9 10 11 |
struct ContentView: View { let gradient = Gradient(stops: [ Gradient.Stop(color: Color.red, location: 0.0), Gradient.Stop(color: Color.green, location: 0.4) ]) var body: some View { RoundedRectangle(cornerRadius: 25) .fill(.linearGradient(gradient, startPoint: .bottom, endPoint: .top)) .frame(width: 100, height: 100) } } |
图11-7:带自定义结束点的线性渐变
除了线性渐变,我们还可以创建各种形状的渐变。例如,通过圆心向外绘制环形图层来创建径向和椭圆渐变,如下所示。
示例11-9:定义环形变量
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { let gradient = Gradient(colors: [Color.red, Color.white]) var body: some View { RoundedRectangle(cornerRadius: 25) .fill(.radialGradient(gradient, center: .center, startRadius: 0, endRadius: 120)) .frame(width: 100, height: 100) } } |
本例描述了如何创建径向渐变。所需要的值有Gradient
结构体、圆心、渐变在形状中开始和结束的位置。这些值指定了渐变开始和结束位置,但仅在形状内的渐变部分才进行绘制。本例中,endRadius
参数指定为120,但因为形状的尺寸是100×100,仅有部分渐变可见。
图11-8:环形渐变
还可以使用另外类型的渐变,角状或锥状渐变。在这些渐变中,延着圆绘制颜色使其从上方看像是圆锥。所需要的值取决于希望定义的圆锥类型。对于简单的锥形,只需要Gradient
结构体、圆心以及渐变起始的角度。
示例11-10:定义锥状渐变
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { let gradient = Gradient(colors: [Color.red, Color.white]) var body: some View { RoundedRectangle(cornerRadius: 25) .fill(.conicGradient(gradient, center: .center, angle: .degrees(180))) .frame(width: 100, height: 100) } } |
渐变的角度通过Angle
结构体的一个实例进行声明。这个结构体包含两类按度数或弧度值进行定义的方法:degrees(Double)
和radians(Double)
。在示例11-10的示例中,渐变的起始角度以180角进行定义,这正是默认起始点的对面。
图11-9:锥形渐变
效果
ShapeStyle
协议中定义了前面小节中实现用于创建渐变结构体的类型方法,也指定了对其它视图应用效果的一些属性和方法。以下是最常用的一些。
- shadow(ShadowStyle):此方法对视图应用阴影。参数是创建内外投影的两个类型方法:
drop(color: Color, radius: CGFloat, x: CGFloat, y: CGFloat)
和inner(color: Color, radius: CGFloat, x: CGFloat, y: CGFloat)
。 - opacity(Double):此方法对视图赋值参数指定的透明度级别。参数接收0.0(全透明)到1.0(不透明)之间的值。
- blendMode(BlendMode):此方法决定了视图与背景及其它视图的混合方式。参数是一个枚举,值有:
normal
、darken
、multiply
、colorBurn
、plusDarker
、lighten
、screen
、colorDodge
、plusLighter
、overlay
、softLight
、hardLight
、difference
、exclusion
、hue
、saturation
、color
、luminosity
、sourceAtop
、destinationOver
和destinationOut
。
很我修饰符可以接收遵循ShapeStyle
协议的结构来为视图赋值样式。在操作形状时,这些样式与foregroundStyle()
修饰符配合更佳。例如,若希望对矩形应用阴影,我们可以使用这一修饰符来实现阴影,使用foregroundStyle()
修饰符定义填充颜色,如下例所示。
示例11-11:对视图添加阴影
1 2 3 4 5 6 7 8 |
struct ContentView: View { var body: some View { RoundedRectangle(cornerRadius: 25) .foregroundStyle(.shadow(.drop(color: .black, radius: 3, x: 4, y: 4))) .foregroundColor(.red) .frame(width: 100, height: 100) } } |
图11-10:阴影
图案
除了颜色和渐变,我们还可以使用图片来填充形状。为此SwiftUI内置了ImagePaint
结构体。该结构体内置了如下类型方法用于创建自定义实例。
- image(Image, sourceRect: CGRect, scale: CGFloat):此方法返回按参数指定的图片和配置的
ImagePaint
结构体。第一个参数提供我们希望使用的图片的Image
视图,sourceRect
参数指定要绘制的图片部分(默认为整个图片),scale
参数定义图片的缩放(默认为原始大小)。
默认,ImagePaint
结构体以原始尺寸使用整个图片,因此大多数情况下指定图片就足以让系统创建图案了,如下例所示。
示例11-12:使用图片填充形状
1 2 3 4 5 6 7 |
struct ContentView: View { var body: some View { Rectangle() .fill(.image(Image(.pattern))) .frame(width: 100, height: 100) } } |
图片无限重复直到填充整个形状。在本例中,我们定义了一个100乘100个点的方块,使用一个25乘25点的图片进行绘制。因为图片小于形状,所以进行了多次绘制覆盖了整个区域。
图11-11:图案
路径
截至目前我们实现的形状都是通过路径定义的。路径是一组定义2D形状轮廓的指令。除了前面由标准形状定义的路径外,我们也可以自行创建。为此,SwiftUI自带了一个Path
视图
路径视图
Path
视图设计用于创建一个包含自定义路径的视图。以下是部分初始化方法。
- Path():此初始化方法创建一个空的
Path
视图。通过对此实例应用修饰符来创建路径。 - Path(Closure):此初始化方法创建一个空的
Path
视图。参数是一个定义路径的闭包。闭包接收我们用于创建路径的Path
结构体的指针。
路径由直线和曲线的组合进行创建。笔触在视图的坐标中从一个点移动到另一个点,就像移动铅笔一样。Path
结构体定义一组修饰符来指定铅笔的位置并生成路径。以下是一些最常用的。
- move(to: CGPoint):此修饰符将铅笔移动到
to
参数所指定的坐标处。 - addLine(to: CGPoint):此修饰符对路径添加一条从铅笔当前位置到
to
参数所指定坐标的直线。 - addLines([CGPoint]):此修饰符向路径添加多条直线。根据数组中点的顺序来逐一添加直线。
- addArc(center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool):此修饰符向路径添加一个圆弧。
center
参数指定圆弧圆心位置的坐标,radius
参数为圆的半径,startAngle
和endAngle
参数为圆弧的起始和结束角度,clockwise
参数指定计算圆弧的方向(true
为顺时针,false
为逆时针)。 - addArc(tangent1End: CGPoint, tangent2End: CGPoint, radius: CGFloat):此修饰符使用切点对路径添加圆弧。
tangent1End
参数指定第一条切线终点的坐标,tangent2End
参数指定第二条切线 终点的坐标,radius
参数指定圆的半径。 - addCurve(to: CGPoint, control1: CGPoint, control2: CGPoint):此修饰符通过两个控制点对路径添加贝塞尔曲线。
to
参数指定终点的坐标,control1
和control2
参数分别定义第一个和第二个控制点的坐标。 - addQuadCurve(to: CGPoint, control: CGPoint):此修饰符通过控制点对路径添加一个二次贝塞尔曲线。
to
参数定义终点的坐标,control
参数定义控制点的坐标。 - addEllipse(in: CGRect):此修饰符对路径添加椭圆。
in
参数定义椭圆的区域。如果矩形为正方形,椭圆就变成了正圆。 - addRect(CGRect):此修饰符向路径添加参数所定义的矩形。这一修饰符还有另一个版本接收一个
CGRect
值的数组,可同时添加多个矩形(addRects([CGRect])
)。 - addRoundedRect(in: CGRect, cornerSize: CGSize, style: RoundedCornerStyle):此修饰符向路径添加一个圆角矩形。
in
参数指定矩形的大小,cornerRadius
参数指定圆角的半径,style
参数是一个枚举,值有circular
和continuous
。
自定义路径与预定义原理相同。如果不指定填充或笔触,所绘制路径的颜色取决于外观模式(浅色模式为黑色,深色模式为白色),但我们可以像对标准形状那样通过fill()
和stroke()
修饰符进行修改。如果使用fill()
修饰符对路径上色,路径会自动关闭,但若是使用stroke()
修饰符,则保持为打开。为关闭路径、保持线为连接状态,Path
结构体内置了如下的修饰符。
- closeSubpath():此修饰符关闭当前路径。如果路径不是闭合路径,它会在终点和起点之间添加一条线来让路径闭合。
要创建路径,我们必须延虚拟的铅笔按顺序应用修饰符。下例中,会创建一个三角形的路径。
示例11-13:定义一个自定义路径
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { var body: some View { Path { path in path.move(to: CGPoint(x: 100, y: 150)) path.addLine(to: CGPoint(x: 200, y: 150)) path.addLine(to: CGPoint(x: 100, y: 250)) path.closeSubpath() }.stroke(Color.blue, lineWidth: 5) } } |
默认,铅笔的初始位置在坐标0, 0(视图的左上角)。如果希望从其它位置开始绘制,必须先应用move()
修饰符。在示例11-13中,我们在添加第一条线前将铅笔移动至坐标100, 150。
后续的线从铅笔当前位置至修饰符所指定的坐标进行创建。例如,在我们的示例中设置起始点后,我们通过该点创建建至200, 150这个点的线,下一条线从该点开始至100, 250结束。注意我们只创建了两条线。从100, 250到100, 150之间的线由closeSubpath()
修饰符自动创建来闭合路径。如果我们希望创建一个开口路径,就可以忽略这一修饰符。
图11-12:自定义路径
通过组合不同的修饰符,我们可以创建复杂的路径。以下的路径由两条线和一个弧形定义。
示例11-14:线和弧形的组合
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { var body: some View { Path { path in path.move(to: CGPoint(x: 100, y: 150)) path.addLine(to: CGPoint(x: 200, y: 150)) path.addArc(center: CGPoint(x: 200, y: 170), radius: 20, startAngle: .degrees(270), endAngle: .degrees(90), clockwise: false) path.addLine(to: CGPoint(x: 100, y: 190)) }.stroke(Color.blue, lineWidth: 5) } } |
因圆弧通过圆心的坐标和半径进行计算,我们必须考虑这两个值将圆弧与之前的线连接。如果圆弧的初始坐标与铅笔的当前位置不匹配,会在这两个点之间创建一条线来连接路径。下面的图11-13显示示例11-14中的例子,以及在将圆心上移10个点(y: 160)后会看到的内容。
图11-13:线与圆弧
addRect()
和addEllipse()
修饰符可用于对路径添加矩形和圆形。该修饰符可对当前路径添加形状,但它们将铅笔移动到CGRect
值所表示的位置,因此被视为独立形状。
示例11-15:组合线和椭圆
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { var body: some View { Path { path in path.move(to: CGPoint(x: 100, y: 150)) path.addLine(to: CGPoint(x: 200, y: 150)) path.addEllipse(in: CGRect(x: 200, y: 140, width: 20, height: 20)) }.stroke(Color.blue, lineWidth: 5) } } |
本例中,在铅笔当前位置和圆之间不会创建直线,除非它们之间是连接的。下面的图11-14展示了示例11-15所获取到的路径,以及将圆形区域向右移动10(x: 210)个后的位置。
图11-14:线和椭圆
除了addArc()
和addEllipse()
,还有另外两个画曲线的修饰符。addQuadCurve()
修饰符绘制一个二次贝塞尔曲线,addCurve()
修饰符生成一个三次贝塞尔曲线。两者的区别在于第一个修饰符只有一个控制点,而第二个有两个,创建不同类型的曲线。
示例11-16:创建复杂曲线
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { var body: some View { Path { path in path.move(to: CGPoint(x: 50, y: 50)) path.addQuadCurve(to: CGPoint(x: 50, y: 200), control: CGPoint(x: 100, y: 125)) path.move(to: CGPoint(x: 250, y: 50)) path.addCurve(to: CGPoint(x: 250, y: 200), control1: CGPoint(x: 200, y: 125), control2: CGPoint(x: 300, y: 125)) }.stroke(Color.blue, lineWidth: 5) } } |
为创建一个二次曲线,我们将铅笔移到50, 50,曲线在50, 200处完结,控制点位于100, 125。使用addCurve()
修饰符创建的三次曲线更为复杂。需要两个控制点,第一个位于200, 125,第二个位于300, 125处。这些点形成的曲线如下所示。
图11-15:复杂曲线
至此我们所创建的路径都使用的是固定值。也就是说不论视图的大小如何,形状尺寸都相同。要让路径按视图大小进行调整,我们需要使用GeometryReader
视图来计算还剩余多少空间(见第8章)。
示例11-17:按容器大小调整路径尺寸
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct ContentView: View { var body: some View { GeometryReader { geometry in Path { path in let width = geometry.size.width / 2 let height = width let posX = (geometry.size.width - width) / 2 let posY = (geometry.size.height - height) / 2 path.move(to: CGPoint(x: posX, y: posY)) path.addLine(to: CGPoint(x: posX + width, y: posY)) path.addLine(to: CGPoint(x: posX, y: posY + height)) path.closeSubpath() }.stroke(Color.blue, lineWidth: 5) } } } |
示例11-7中的代码绘制了一个宽度为容器一半宽度的三角形。首先,我们通过对容器宽度除2来计算三角形的宽度。然后将这个值赋值给常量height
来设置高度与宽度相等。在计算好大小后,我们来获取起点的位置。因为希望让三角形在容器中居中,我们通过容器宽度减去三角形的宽度再除以2获取起点。纵轴位置做相同的操作,分别存储到常量posX
和posY
中。最终通过这些值绘制出路径。move()
修饰符将铅笔移到posX
和posY
所定义的起点。然后addLine()
修饰符从该点绘制一条线到三角形的右端(posX + width
)。下一个addLine()
修饰符从该点绘制一条线到三角形的左下角(posY + height
)。最终closeSubPath()
修饰符绘制一条垂线闭合路径。
因为路径的所有坐标都通过容器的值进行计算,三角形会按照容器的尺寸进行调整,其大小保持为其尺寸的一半并在视图中居中。
图11-16:相对尺寸的路径
自定义形状
本章开头介绍的通用形状是遵循Shape
协议的结构体。遵循该协议的结构体定义自身的开着,创建方式 与Path
视力的路径相同,但使用Shape
结构体代替Path
视图的优势 是Shape
结构体具备绘制形状所处视图大小的CGRect
值 ,因此形状会保持适配窗口的大小(我们无需使用GeometryReader
视图来计算其大小 )。
该协议要求结构体实现如下方法来定义形状的路径。
- path(in: CGRect):此方法接收 一个视力尺寸的
CGRect
值 ,必须返回一个希望赋值给开着的路径Path
视图。
创建自定义形状很简单。要定义一个遵循Shape
协议的结构体,实现path()
方法,创建并返回一个Path
视图。下例定义一个绘制三角形的Triangle
形状结构体。
示例11-18:创建一个自定义形状视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import SwiftUI struct Triangle: Shape { func path(in rect: CGRect) -> Path { var path = Path() let width = rect.width let height = rect.height let posX = rect.origin.x let posY = rect.origin.y path.move(to: CGPoint(x: posX, y: posY)) path.addLine(to: CGPoint(x: posX + width, y: posY)) path.addLine(to: CGPoint(x: posX, y: posY + height)) path.closeSubpath() return path } } |
这里的路径与前例相同,但现在是拿方法所接收到的CGRect
值来计算形状的尺寸。本例中我将三角形扩展为从左到右、从上到下覆盖整个视图。(三角形的大小对应视图的宽和高,这是自定义形状推荐的方式。)
定义好Shape
视图后,我们可以像其它视图一样在界面中进行实现。为进行演示,我们在横向的ScrollView
视图中实例化多个不同尺寸的Triangle
视图。
示例11-19:实现自定义形状视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
struct ContentView: View { var body: some View { VStack { ScrollView(.horizontal, showsIndicators: true) { HStack { Triangle() .fill(Color.blue) .frame(width: 120, height: 50) Triangle() .fill(Color.green) .frame(width: 120, height: 100) Triangle() .fill(Color.yellow) .frame(width: 120, height: 80) Triangle() .fill(Color.red) .frame(width: 50, height: 50) } }.padding() Spacer() } } } |
创建Triangle
视图时,使用视图的大小调用path()
方法,根据这些值来绘制三角形。因此,如果定义不同大小的Triangle
视图,会在屏幕上得到不同形状的三角形。
图11-17:自定义形状视图
✍️跟我一起做:使用示例11-18中的代码创建一个Swift文件Triangle.swift
。再用示例11-19中的代码更新ContentView
视图。如果看不全所有的三角形,滚动视图或旋转设备(参见图11-17)。
变换
SwiftUI提供了各种修改视图或Shape
形状物理特征的工具,如朝向、透视或内置的位置。以下是其中的一些修饰符。
- offset(CGSize):该修饰符按参数定义的横向和垂直距离放置视图的内容。
- rotationEffect(Angle):该修饰符旋转视图的内容至参数所指定的角度。
- rotation3DEffect(Angle, Tuple):该修饰符在3D中旋转视图的内容。第一个参数以度数或弧度声明角度,第二个参数是一个带三个参数表示坐标轴的无级,如
(x: Double, y: Double, z: Double)
。不为零的值会在该轴上旋转图像。 - clipShape(Shape):该修饰符使用参数指定的形状修剪视图。
这些修饰符影响视图的内容。例如,如果对Image
视图应用offset
,视图中的图像就会按修饰符指定的距离进行移动,但视图的边框不受影响。
示例11-20:放置图像
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { var body: some View { Image(.spot1) .resizable() .scaledToFit() .frame(width: 150, height: 200) .offset(CGSize(width: 75, height: 0)) } } |
示例11-20中的代码将图像向右移动75个点(Image
视图宽的一半)。
图11-18:图像靠右放置
旋转修饰符的运作方式类似。它们会旋转2D或3D视图中的内容。最有意思的要数rotation3DEffect()
,可在任意一条轴上旋转内容。
示例11-21:旋转图像
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { var body: some View { Image(.spot1) .resizable() .scaledToFit() .frame(width: 150, height: 200) .scaleEffect(CGSize(width: 0.9, height: 0.9)) .rotation3DEffect(.degrees(30), axis: (x: 0.0, y: 1.0, z: 0.0)) } } |
rotation3DEffect()
修饰符需要一个指定内容所要围绕旋转坐标轴的元组。值为0时表示无需旋转,非0的值表示旋转的方式,负值朝一侧,正值朝另一侧。
图11-19:延y轴旋转的图像
画布
图表
标记视图
图表修饰符
选择
多标记
滚动
图像渲染器
动画
自定义形状动画
画布动画
过渡
SF图标动画
翻译整理中…
代码请见:GitHub仓库