SwiftUI - 在视图之间绘制(弯曲的)路径
SwiftUI - Drawing (curved) paths between views
用非常基本的术语来说,在我的 Android 应用程序中,我有一个屏幕可以绘制圆圈,然后用曲线将它们连接起来。
我正在尝试在 SwiftUI 中重新创建它。
我发现这个问题似乎与我正在寻找的问题非常相似,但不幸的是答案非常简短,甚至在阅读了大约 10 个不同的博客和 5 个视频之后,我仍然没有完全理解它。
所以基本逻辑是我以某种方式使用 GeometryReader
获取我创建的每个 Circle
的 .midX
和 .midY
坐标,然后绘制 Paths
它们之间。 我唯一的问题是在创建圆后获取这些坐标。
以及如何将路径添加到屏幕,在 ZStack
中,Circles
在前面,路径作为一个自定义形状在后面?
更多信息:
在 Android 上,最终结果如下所示:
基本上我有一个 Challenge
object,它有一个名字和一些详细的文字,我把它们排成这样,这样它在视觉上代表了用户的“旅程”。
所以我真正需要的是知道如何布置一些 circles/images(上面有文字)然后画线连接它们。每个这样的挑战圈都需要可点击才能打开详细视图。
GeometryReader
给你信息,如果容器视图,你可以得到像 geometry.size
这样的大小,然后计算中点等
内部 GeometryReader
布局是 ZStack
,因此所有项目都将一个放在另一个之上
绘制曲线的简单方法是 Path { path in }
,在此块中,您可以将 lines/curves 添加到路径,而不是 stoke()
它
您可以通过两种方式绘制圆:第一种是再次使用 Path
,添加圆角矩形并 fill()
它。
另一种选择是放置 Circle()
并添加一个偏移量
我用第一种方式用蓝色,用第二种方式用绿色,半径较小。我随机选择了曲线控制点只是为了给你一个想法
let circleRelativeCenters = [
CGPoint(x: 0.8, y: 0.2),
CGPoint(x: 0.2, y: 0.5),
CGPoint(x: 0.8, y: 0.8),
]
var body: some View {
GeometryReader { geometry in
let normalizedCenters = circleRelativeCenters
.map { center in
CGPoint(
x: center.x * geometry.size.width,
y: center.y * geometry.size.height
)
}
Path { path in
var prevPoint = CGPoint(x: normalizedCenters[0].x / 4, y: normalizedCenters[0].y / 2)
path.move(to: prevPoint)
normalizedCenters.forEach { center in
path.addQuadCurve(
to: center,
control: .init(
x: (center.x + prevPoint.x) / 2,
y: (center.y - prevPoint.y) / 2)
)
prevPoint = center
}
}.stroke(lineWidth: 3).foregroundColor(.blue).background(Color.yellow)
Path { path in
let circleDiamter = geometry.size.width / 5
let circleFrameSize = CGSize(width: circleDiamter, height: circleDiamter)
let circleCornerSize = CGSize(width: circleDiamter / 2, height: circleDiamter / 2)
normalizedCenters.forEach { center in
path.addRoundedRect(
in: CGRect(
origin: CGPoint(
x: center.x - circleFrameSize.width / 2,
y: center.y - circleFrameSize.width / 2
), size: circleFrameSize
),
cornerSize: circleCornerSize
)
}
}.fill()
ForEach(normalizedCenters.indices, id: \.self) { i in
let center = normalizedCenters[i]
let circleDiamter = geometry.size.width / 6
let circleFrameSize = CGSize(width: circleDiamter, height: circleDiamter)
Circle()
.frame(size: circleFrameSize)
.offset(
x: center.x - circleFrameSize.width / 2,
y: center.y - circleFrameSize.width / 2
)
}.foregroundColor(.green)
}.frame(maxWidth: .infinity, maxHeight: .infinity).foregroundColor(.blue).background(Color.yellow)
}
结果:
在 Path { path in
内部我可以使用 forEach
,因为它不再是视图构建器的范围。
如果您需要对修饰符进行一些计算,可以使用下一个技巧:
func circles(geometry: GeometryProxy) -> some View {
var points = [CGPoint]()
var prevPoint: CGPoint?
(0...5).forEach { i in
let point: CGPoint
if let prevPoint = prevPoint {
point = CGPoint(x: prevPoint.x + 1, y: prevPoint.y)
} else {
point = .zero
}
points.append(point)
prevPoint = point
}
return ForEach(points.indices, id: \.self) { i in
let point = points[i]
Circle()
.offset(
x: point.x,
y: point.y
)
}
}
然后你可以像circles(geometry: geometry).foregroundColor(.green)
一样在正文中使用它
我正在使用 preferenceKey (CirclePointsKey
) 的 reduce function
将所有坐标存储到一个点数组中。带有几何 reader 的叠加层将读取每个球的每个中间位置的位置。我将视图容器框架命名为 ballContainer
只是为了获得正确的相对位置。
这与您发布的曲线不完全相同,但您可以根据需要更改“path.addCurve”中的参数。
只有当至少有 2 个 preferenceKey 尝试设置新值时,才会调用 reduce 函数。通常用于设置新值,但在本例中我附加了每个值。
struct CirclePointsKey: PreferenceKey {
typealias Value = [CGPoint]
static var defaultValue: [CGPoint] = []
static func reduce(value: inout [CGPoint], nextValue: () -> [CGPoint]) {
value.append(contentsOf: nextValue())
}
}
struct ExampleView: View {
@State var points: [CGPoint] = []
var body: some View {
ZStack {
ScrollView {
ZStack {
Path { (path: inout Path) in
if let firstPoint = points.first {
path.move(to: firstPoint)
var lastPoint: CGPoint = firstPoint
for point in points.dropFirst() {
// path.addLine(to: point)
let isGoingRight = point.x < lastPoint.x
path.addCurve(to: point, control1: CGPoint(x: isGoingRight ? point.x : lastPoint.x,
y: !isGoingRight ? point.y : lastPoint.y),
control2: CGPoint(x: point.x, y: point.y))
lastPoint = point
}
}
}
.stroke(lineWidth: 2)
.foregroundColor(.white.opacity(0.5))
VStack {
VStack(spacing: 30) {
ForEach(Array(0...4).indices) { index in
ball
.overlay(GeometryReader { geometry in
Color.clear
.preference(key: CirclePointsKey.self,
value: [CGPoint(x: geometry.frame(in: .named("ballContainer")).midX,
y: geometry.frame(in: .named("ballContainer")).midY)])
})
.frame(maxWidth: .infinity,
alignment: index.isMultiple(of: 2) ? .leading : .trailing)
}
}
.coordinateSpace(name: "ballContainer")
.onPreferenceChange(CirclePointsKey.self) { data in
points = data
}
Text("points:\n \(String(describing: points))")
Spacer()
}
}
}
}
.foregroundColor(.white)
.background(Color.black.ignoresSafeArea())
}
@ViewBuilder
var ball: some View {
Circle()
.fill(Color.gray)
.frame(width: 70, height: 70, alignment: .center)
.padding(10)
.overlay(Circle()
.strokeBorder(style: StrokeStyle(lineWidth: 2, dash: [5, 10, 10, 5]))
.foregroundColor(.white)
.padding(7)
)
.shadow(color: .white.opacity(0.7), radius: 10, x: 0.0, y: 0.0)
}
}
用非常基本的术语来说,在我的 Android 应用程序中,我有一个屏幕可以绘制圆圈,然后用曲线将它们连接起来。
我正在尝试在 SwiftUI 中重新创建它。
我发现这个问题似乎与我正在寻找的问题非常相似,但不幸的是答案非常简短,甚至在阅读了大约 10 个不同的博客和 5 个视频之后,我仍然没有完全理解它。
所以基本逻辑是我以某种方式使用 GeometryReader
获取我创建的每个 Circle
的 .midX
和 .midY
坐标,然后绘制 Paths
它们之间。 我唯一的问题是在创建圆后获取这些坐标。
以及如何将路径添加到屏幕,在 ZStack
中,Circles
在前面,路径作为一个自定义形状在后面?
更多信息:
在 Android 上,最终结果如下所示:
基本上我有一个 Challenge
object,它有一个名字和一些详细的文字,我把它们排成这样,这样它在视觉上代表了用户的“旅程”。
所以我真正需要的是知道如何布置一些 circles/images(上面有文字)然后画线连接它们。每个这样的挑战圈都需要可点击才能打开详细视图。
GeometryReader
给你信息,如果容器视图,你可以得到像 geometry.size
这样的大小,然后计算中点等
内部 GeometryReader
布局是 ZStack
,因此所有项目都将一个放在另一个之上
绘制曲线的简单方法是 Path { path in }
,在此块中,您可以将 lines/curves 添加到路径,而不是 stoke()
它
您可以通过两种方式绘制圆:第一种是再次使用 Path
,添加圆角矩形并 fill()
它。
另一种选择是放置 Circle()
并添加一个偏移量
我用第一种方式用蓝色,用第二种方式用绿色,半径较小。我随机选择了曲线控制点只是为了给你一个想法
let circleRelativeCenters = [
CGPoint(x: 0.8, y: 0.2),
CGPoint(x: 0.2, y: 0.5),
CGPoint(x: 0.8, y: 0.8),
]
var body: some View {
GeometryReader { geometry in
let normalizedCenters = circleRelativeCenters
.map { center in
CGPoint(
x: center.x * geometry.size.width,
y: center.y * geometry.size.height
)
}
Path { path in
var prevPoint = CGPoint(x: normalizedCenters[0].x / 4, y: normalizedCenters[0].y / 2)
path.move(to: prevPoint)
normalizedCenters.forEach { center in
path.addQuadCurve(
to: center,
control: .init(
x: (center.x + prevPoint.x) / 2,
y: (center.y - prevPoint.y) / 2)
)
prevPoint = center
}
}.stroke(lineWidth: 3).foregroundColor(.blue).background(Color.yellow)
Path { path in
let circleDiamter = geometry.size.width / 5
let circleFrameSize = CGSize(width: circleDiamter, height: circleDiamter)
let circleCornerSize = CGSize(width: circleDiamter / 2, height: circleDiamter / 2)
normalizedCenters.forEach { center in
path.addRoundedRect(
in: CGRect(
origin: CGPoint(
x: center.x - circleFrameSize.width / 2,
y: center.y - circleFrameSize.width / 2
), size: circleFrameSize
),
cornerSize: circleCornerSize
)
}
}.fill()
ForEach(normalizedCenters.indices, id: \.self) { i in
let center = normalizedCenters[i]
let circleDiamter = geometry.size.width / 6
let circleFrameSize = CGSize(width: circleDiamter, height: circleDiamter)
Circle()
.frame(size: circleFrameSize)
.offset(
x: center.x - circleFrameSize.width / 2,
y: center.y - circleFrameSize.width / 2
)
}.foregroundColor(.green)
}.frame(maxWidth: .infinity, maxHeight: .infinity).foregroundColor(.blue).background(Color.yellow)
}
结果:
在 Path { path in
内部我可以使用 forEach
,因为它不再是视图构建器的范围。
如果您需要对修饰符进行一些计算,可以使用下一个技巧:
func circles(geometry: GeometryProxy) -> some View {
var points = [CGPoint]()
var prevPoint: CGPoint?
(0...5).forEach { i in
let point: CGPoint
if let prevPoint = prevPoint {
point = CGPoint(x: prevPoint.x + 1, y: prevPoint.y)
} else {
point = .zero
}
points.append(point)
prevPoint = point
}
return ForEach(points.indices, id: \.self) { i in
let point = points[i]
Circle()
.offset(
x: point.x,
y: point.y
)
}
}
然后你可以像circles(geometry: geometry).foregroundColor(.green)
我正在使用 preferenceKey (CirclePointsKey
) 的 reduce function
将所有坐标存储到一个点数组中。带有几何 reader 的叠加层将读取每个球的每个中间位置的位置。我将视图容器框架命名为 ballContainer
只是为了获得正确的相对位置。
这与您发布的曲线不完全相同,但您可以根据需要更改“path.addCurve”中的参数。
只有当至少有 2 个 preferenceKey 尝试设置新值时,才会调用 reduce 函数。通常用于设置新值,但在本例中我附加了每个值。
struct CirclePointsKey: PreferenceKey {
typealias Value = [CGPoint]
static var defaultValue: [CGPoint] = []
static func reduce(value: inout [CGPoint], nextValue: () -> [CGPoint]) {
value.append(contentsOf: nextValue())
}
}
struct ExampleView: View {
@State var points: [CGPoint] = []
var body: some View {
ZStack {
ScrollView {
ZStack {
Path { (path: inout Path) in
if let firstPoint = points.first {
path.move(to: firstPoint)
var lastPoint: CGPoint = firstPoint
for point in points.dropFirst() {
// path.addLine(to: point)
let isGoingRight = point.x < lastPoint.x
path.addCurve(to: point, control1: CGPoint(x: isGoingRight ? point.x : lastPoint.x,
y: !isGoingRight ? point.y : lastPoint.y),
control2: CGPoint(x: point.x, y: point.y))
lastPoint = point
}
}
}
.stroke(lineWidth: 2)
.foregroundColor(.white.opacity(0.5))
VStack {
VStack(spacing: 30) {
ForEach(Array(0...4).indices) { index in
ball
.overlay(GeometryReader { geometry in
Color.clear
.preference(key: CirclePointsKey.self,
value: [CGPoint(x: geometry.frame(in: .named("ballContainer")).midX,
y: geometry.frame(in: .named("ballContainer")).midY)])
})
.frame(maxWidth: .infinity,
alignment: index.isMultiple(of: 2) ? .leading : .trailing)
}
}
.coordinateSpace(name: "ballContainer")
.onPreferenceChange(CirclePointsKey.self) { data in
points = data
}
Text("points:\n \(String(describing: points))")
Spacer()
}
}
}
}
.foregroundColor(.white)
.background(Color.black.ignoresSafeArea())
}
@ViewBuilder
var ball: some View {
Circle()
.fill(Color.gray)
.frame(width: 70, height: 70, alignment: .center)
.padding(10)
.overlay(Circle()
.strokeBorder(style: StrokeStyle(lineWidth: 2, dash: [5, 10, 10, 5]))
.foregroundColor(.white)
.padding(7)
)
.shadow(color: .white.opacity(0.7), radius: 10, x: 0.0, y: 0.0)
}
}