ios 如何在SwiftUI中的图像上绘制线条

oknwwptz  于 2023-11-19  发布在  iOS
关注(0)|答案(2)|浏览(200)

我想使用SwiftUI在图像上交互式地画一条线。
我遇到了几个问题。我开始在图像上使用DragGesture。然而,图像上的点偏离了Path想要绘制的位置。这主要是通过在覆盖层中绘制来解决的-这将与图像的大小和位置相同。
下面的解决方案还以像素和屏幕单位获取图像大小,以帮助进行坐标转换。

d4so4syb

d4so4syb1#

我建议你用DragGesture来记录点,用Canvas来画线。
您描述的路径上的点与捕获它们的位置不对应的问题是使用不同坐标空间的症状。您需要在捕获点的同一坐标空间中绘制线。例如:

  • DragGesture连接到Image,并在Image上绘制重叠线
  • 或者,将DragGesture连接到包含ImageZStack,并在ZStack内部的层中绘制线。

下面是一个简单的实现,它在连续拖动手势的端点之间绘制直线。它使用了上述两种方法中的第二种,使用ZStack

  1. struct ContentView: View {
  2. @State private var points = [CGPoint]()
  3. @State private var startOfDrag = CGPoint.zero
  4. var body: some View {
  5. ZStack {
  6. Image(systemName: "ladybug")
  7. .resizable()
  8. .scaledToFit()
  9. if points.count > 1 {
  10. Canvas { ctx, size in
  11. var path = Path()
  12. path.addLines(points)
  13. ctx.stroke(path, with: .color(.red), lineWidth: 5)
  14. }
  15. }
  16. }
  17. .gesture(
  18. DragGesture(minimumDistance: 0)
  19. .onChanged { val in
  20. if points.isEmpty || startOfDrag != val.startLocation {
  21. startOfDrag = val.startLocation
  22. points.append(startOfDrag)
  23. } else if val.translation != .zero {
  24. if points.count > 1 {
  25. points.removeLast()
  26. }
  27. points.append(val.location)
  28. }
  29. }
  30. )
  31. .frame(width: 300, height: 300)
  32. }
  33. }

字符串


的数据
这一解决办法可以以各种方式加以阐述,例如:

  • 收集所有的点从一个拖动运动,以建立一个更详细的路径(见Create a pencil effect for drawing on SwiftUi
  • 使用GeometryReader获取拖动区域的大小,并将这些点转换为相对UnitPoint位置,以便它们可以用于在已调整大小的图像上绘制相同的线
  • 当最后一个点靠近第一个点时,允许路径闭合
  • 提供一个撤消按钮,允许删除路径的最后一行。
展开查看全部
nhaq1z21

nhaq1z212#


的数据
下面的代码让我们在图像的顶部绘制一条直线路径,如上图所示。直线的点被归一化到图像,这样即使在图像大小发生变化(例如设备旋转后可能发生的变化)后,也可以正确地重新绘制直线。
涉及的技巧包括在覆盖层中绘制线条。代码还显示了以像素为单位的图像大小和以点为单位的图像视图大小。这些大小在不同视图之间转换时很有用。

  1. import SwiftUI
  2. struct ContentView: View {
  3. @State private var line: [CGPoint] = [] // Line is an array of points
  4. @State private var pixelSize = PixelSize()
  5. @State private var viewSize = CGSize.zero
  6. @State private var startNewSegment = true
  7. @State private var pixelPath = ""
  8. var body: some View {
  9. VStack {
  10. lineView() // Magic happens here
  11. buttons()
  12. info()
  13. }
  14. .padding()
  15. .onAppear {
  16. // Comment out next line when no longer needed for debugging
  17. line = [CGPoint(x: 0, y: 0), CGPoint(x: 0.25, y: 0.25), CGPoint(x: 0.5, y: 0)]
  18. }
  19. }
  20. @ViewBuilder
  21. func lineView() -> some View {
  22. let largeConfig = UIImage.SymbolConfiguration(pointSize: 100, weight: .bold, scale: .large)
  23. let image = UIImage(systemName: "bolt.square", withConfiguration: largeConfig)! // Your image here
  24. Image(uiImage: image)
  25. .resizable()
  26. .aspectRatio(contentMode: .fit)
  27. .saveSize(in: $viewSize) // NOTE: this is the screen size for the image!
  28. .overlay { // Draw line in overlay. You really want to do this.
  29. if line.count > 1 {
  30. // Replace "Path" with "Canvas"
  31. Canvas { ctx, size in
  32. var path = Path()
  33. path.addLines(line.map { screenPoint($0)})
  34. ctx.stroke(path, with: .color(.blue), lineWidth: 5)
  35. }
  36. // Path { path in
  37. // path.move(to: screenPoint(line[0]))
  38. // for i in 1..<line.count {
  39. // path.addLine(to: screenPoint(line[i]))
  40. // }
  41. // }
  42. // .stroke(.blue, lineWidth: 5)
  43. }
  44. }
  45. .onAppear {
  46. pixelSize = image.pixelSize
  47. }
  48. // Build up line by adding points in straight line segments.
  49. // Allow point to be added by tap
  50. .onTapGesture { location in
  51. line.append(limitPoint(location))
  52. }
  53. // Or allow new point to be added from drag
  54. .gesture(
  55. DragGesture()
  56. .onChanged { value in
  57. if line.count < 1 {
  58. // If no points, add "startLocation" point (more accurate than simply location for first point)
  59. line.append(limitPoint(value.startLocation))
  60. } else if line.count < 2 || startNewSegment {
  61. // Add point at current position
  62. line.append(limitPoint(value.location))
  63. startNewSegment = false
  64. } else {
  65. // Note: Now in mode where we are replacing the last point
  66. line.removeLast()
  67. line.append(limitPoint(value.location))
  68. startNewSegment = false
  69. }
  70. }
  71. .onEnded { value in
  72. line.removeLast()
  73. line.append(limitPoint(value.location))
  74. startNewSegment = true
  75. }
  76. )
  77. }
  78. func screenPoint(_ point: CGPoint) -> CGPoint {
  79. // Convert 0->1 to view's coordinates
  80. let vw = viewSize.width
  81. let vh = viewSize.height
  82. let nextX = min(1, max(0, point.x)) * vw
  83. let nextY = min(1, max(0, point.y)) * vh
  84. return CGPoint(x: nextX, y: nextY)
  85. }
  86. func limitPoint(_ point: CGPoint) -> CGPoint {
  87. // Convert view coordinate to normalized 0->1 range
  88. let vw = max(viewSize.width, 1)
  89. let vh = max(viewSize.height, 1)
  90. // Keep in bounds - even if dragging outside of bounds
  91. let nextX = min(1, max(0, point.x / vw))
  92. let nextY = min(1, max(0, point.y / vh))
  93. return CGPoint(x: nextX, y: nextY)
  94. }
  95. @ViewBuilder
  96. func buttons() -> some View {
  97. HStack {
  98. Button {
  99. line.removeAll()
  100. pixelPath = ""
  101. } label: {
  102. Text("Clear")
  103. .padding()
  104. }
  105. Button {
  106. // Show line points in "Pixel" units
  107. let vw = viewSize.width
  108. let vh = viewSize.height
  109. if vw > 0 && vh > 0 {
  110. let pixelWidth = CGFloat(pixelSize.width - 1)
  111. let pixelHeight = CGFloat(pixelSize.height - 1)
  112. let pixelPoints = line.map { CGPoint(x: pixelWidth * $0.x, y: pixelHeight * $0.y)}
  113. pixelPath = "\(pixelPoints)"
  114. }
  115. } label: {
  116. Text("Pixel Path")
  117. .padding()
  118. }
  119. }
  120. }
  121. @ViewBuilder
  122. func info() -> some View {
  123. Text("Image WxL: \(pixelSize.width) x \(pixelSize.height)")
  124. Text("View WxL: \(viewSize.width) x \(viewSize.height)")
  125. Text("Line points: \(line.count)")
  126. if pixelPath != "" {
  127. Text("Pixel Path: \(pixelPath)")
  128. }
  129. }
  130. }
  131. // Auxiliary definitions and functions
  132. struct PixelSize {
  133. var width: Int = 0
  134. var height: Int = 0
  135. }
  136. extension UIImage {
  137. var pixelSize: PixelSize {
  138. if let cgImage = cgImage {
  139. return PixelSize(width: cgImage.width, height: cgImage.height)
  140. }
  141. return PixelSize()
  142. }
  143. }
  144. // SizeCalculator from: https://stackoverflow.com/questions/57577462/get-width-of-a-view-using-in-swiftui
  145. struct SizeCalculator: ViewModifier {
  146. @Binding var size: CGSize
  147. func body(content: Content) -> some View {
  148. content
  149. .background(
  150. GeometryReader { proxy in
  151. Color.clear // we just want the reader to get triggered, so let's use an empty color
  152. .onAppear {
  153. size = proxy.size
  154. }
  155. .onChange(of: proxy.size) { // Added to handle device rotation
  156. size = proxy.size
  157. }
  158. }
  159. )
  160. }
  161. }
  162. extension View {
  163. func saveSize(in size: Binding<CGSize>) -> some View {
  164. modifier(SizeCalculator(size: size))
  165. }
  166. }

字符串

展开查看全部

相关问题