我想使用SwiftUI在图像上交互式地画一条线。我遇到了几个问题。我开始在图像上使用DragGesture。然而,图像上的点偏离了Path想要绘制的位置。这主要是通过在覆盖层中绘制来解决的-这将与图像的大小和位置相同。下面的解决方案还以像素和屏幕单位获取图像大小,以帮助进行坐标转换。
d4so4syb1#
我建议你用DragGesture来记录点,用Canvas来画线。您描述的路径上的点与捕获它们的位置不对应的问题是使用不同坐标空间的症状。您需要在捕获点的同一坐标空间中绘制线。例如:
DragGesture
Canvas
Image
ZStack
下面是一个简单的实现,它在连续拖动手势的端点之间绘制直线。它使用了上述两种方法中的第二种,使用ZStack:
struct ContentView: View { @State private var points = [CGPoint]() @State private var startOfDrag = CGPoint.zero var body: some View { ZStack { Image(systemName: "ladybug") .resizable() .scaledToFit() if points.count > 1 { Canvas { ctx, size in var path = Path() path.addLines(points) ctx.stroke(path, with: .color(.red), lineWidth: 5) } } } .gesture( DragGesture(minimumDistance: 0) .onChanged { val in if points.isEmpty || startOfDrag != val.startLocation { startOfDrag = val.startLocation points.append(startOfDrag) } else if val.translation != .zero { if points.count > 1 { points.removeLast() } points.append(val.location) } } ) .frame(width: 300, height: 300) }}
struct ContentView: View {
@State private var points = [CGPoint]()
@State private var startOfDrag = CGPoint.zero
var body: some View {
ZStack {
Image(systemName: "ladybug")
.resizable()
.scaledToFit()
if points.count > 1 {
Canvas { ctx, size in
var path = Path()
path.addLines(points)
ctx.stroke(path, with: .color(.red), lineWidth: 5)
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { val in
if points.isEmpty || startOfDrag != val.startLocation {
startOfDrag = val.startLocation
points.append(startOfDrag)
} else if val.translation != .zero {
points.removeLast()
points.append(val.location)
)
.frame(width: 300, height: 300)
字符串
的数据这一解决办法可以以各种方式加以阐述,例如:
GeometryReader
UnitPoint
nhaq1z212#
的数据下面的代码让我们在图像的顶部绘制一条直线路径,如上图所示。直线的点被归一化到图像,这样即使在图像大小发生变化(例如设备旋转后可能发生的变化)后,也可以正确地重新绘制直线。涉及的技巧包括在覆盖层中绘制线条。代码还显示了以像素为单位的图像大小和以点为单位的图像视图大小。这些大小在不同视图之间转换时很有用。
import SwiftUIstruct ContentView: View { @State private var line: [CGPoint] = [] // Line is an array of points @State private var pixelSize = PixelSize() @State private var viewSize = CGSize.zero @State private var startNewSegment = true @State private var pixelPath = "" var body: some View { VStack { lineView() // Magic happens here buttons() info() } .padding() .onAppear { // Comment out next line when no longer needed for debugging line = [CGPoint(x: 0, y: 0), CGPoint(x: 0.25, y: 0.25), CGPoint(x: 0.5, y: 0)] } } @ViewBuilder func lineView() -> some View { let largeConfig = UIImage.SymbolConfiguration(pointSize: 100, weight: .bold, scale: .large) let image = UIImage(systemName: "bolt.square", withConfiguration: largeConfig)! // Your image here Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fit) .saveSize(in: $viewSize) // NOTE: this is the screen size for the image! .overlay { // Draw line in overlay. You really want to do this. if line.count > 1 { // Replace "Path" with "Canvas" Canvas { ctx, size in var path = Path() path.addLines(line.map { screenPoint($0)}) ctx.stroke(path, with: .color(.blue), lineWidth: 5) } // Path { path in // path.move(to: screenPoint(line[0])) // for i in 1..<line.count { // path.addLine(to: screenPoint(line[i])) // } // } // .stroke(.blue, lineWidth: 5) } } .onAppear { pixelSize = image.pixelSize } // Build up line by adding points in straight line segments. // Allow point to be added by tap .onTapGesture { location in line.append(limitPoint(location)) } // Or allow new point to be added from drag .gesture( DragGesture() .onChanged { value in if line.count < 1 { // If no points, add "startLocation" point (more accurate than simply location for first point) line.append(limitPoint(value.startLocation)) } else if line.count < 2 || startNewSegment { // Add point at current position line.append(limitPoint(value.location)) startNewSegment = false } else { // Note: Now in mode where we are replacing the last point line.removeLast() line.append(limitPoint(value.location)) startNewSegment = false } } .onEnded { value in line.removeLast() line.append(limitPoint(value.location)) startNewSegment = true } ) } func screenPoint(_ point: CGPoint) -> CGPoint { // Convert 0->1 to view's coordinates let vw = viewSize.width let vh = viewSize.height let nextX = min(1, max(0, point.x)) * vw let nextY = min(1, max(0, point.y)) * vh return CGPoint(x: nextX, y: nextY) } func limitPoint(_ point: CGPoint) -> CGPoint { // Convert view coordinate to normalized 0->1 range let vw = max(viewSize.width, 1) let vh = max(viewSize.height, 1) // Keep in bounds - even if dragging outside of bounds let nextX = min(1, max(0, point.x / vw)) let nextY = min(1, max(0, point.y / vh)) return CGPoint(x: nextX, y: nextY) } @ViewBuilder func buttons() -> some View { HStack { Button { line.removeAll() pixelPath = "" } label: { Text("Clear") .padding() } Button { // Show line points in "Pixel" units let vw = viewSize.width let vh = viewSize.height if vw > 0 && vh > 0 { let pixelWidth = CGFloat(pixelSize.width - 1) let pixelHeight = CGFloat(pixelSize.height - 1) let pixelPoints = line.map { CGPoint(x: pixelWidth * $0.x, y: pixelHeight * $0.y)} pixelPath = "\(pixelPoints)" } } label: { Text("Pixel Path") .padding() } } } @ViewBuilder func info() -> some View { Text("Image WxL: \(pixelSize.width) x \(pixelSize.height)") Text("View WxL: \(viewSize.width) x \(viewSize.height)") Text("Line points: \(line.count)") if pixelPath != "" { Text("Pixel Path: \(pixelPath)") } }}// Auxiliary definitions and functionsstruct PixelSize { var width: Int = 0 var height: Int = 0}extension UIImage { var pixelSize: PixelSize { if let cgImage = cgImage { return PixelSize(width: cgImage.width, height: cgImage.height) } return PixelSize() }}// SizeCalculator from: https://stackoverflow.com/questions/57577462/get-width-of-a-view-using-in-swiftuistruct SizeCalculator: ViewModifier { @Binding var size: CGSize func body(content: Content) -> some View { content .background( GeometryReader { proxy in Color.clear // we just want the reader to get triggered, so let's use an empty color .onAppear { size = proxy.size } .onChange(of: proxy.size) { // Added to handle device rotation size = proxy.size } } ) }}extension View { func saveSize(in size: Binding<CGSize>) -> some View { modifier(SizeCalculator(size: size)) }}
import SwiftUI
@State private var line: [CGPoint] = [] // Line is an array of points
@State private var pixelSize = PixelSize()
@State private var viewSize = CGSize.zero
@State private var startNewSegment = true
@State private var pixelPath = ""
VStack {
lineView() // Magic happens here
buttons()
info()
.padding()
.onAppear {
// Comment out next line when no longer needed for debugging
line = [CGPoint(x: 0, y: 0), CGPoint(x: 0.25, y: 0.25), CGPoint(x: 0.5, y: 0)]
@ViewBuilder
func lineView() -> some View {
let largeConfig = UIImage.SymbolConfiguration(pointSize: 100, weight: .bold, scale: .large)
let image = UIImage(systemName: "bolt.square", withConfiguration: largeConfig)! // Your image here
Image(uiImage: image)
.aspectRatio(contentMode: .fit)
.saveSize(in: $viewSize) // NOTE: this is the screen size for the image!
.overlay { // Draw line in overlay. You really want to do this.
if line.count > 1 {
// Replace "Path" with "Canvas"
path.addLines(line.map { screenPoint($0)})
ctx.stroke(path, with: .color(.blue), lineWidth: 5)
// Path { path in
// path.move(to: screenPoint(line[0]))
// for i in 1..<line.count {
// path.addLine(to: screenPoint(line[i]))
// }
// .stroke(.blue, lineWidth: 5)
pixelSize = image.pixelSize
// Build up line by adding points in straight line segments.
// Allow point to be added by tap
.onTapGesture { location in
line.append(limitPoint(location))
// Or allow new point to be added from drag
DragGesture()
.onChanged { value in
if line.count < 1 {
// If no points, add "startLocation" point (more accurate than simply location for first point)
line.append(limitPoint(value.startLocation))
} else if line.count < 2 || startNewSegment {
// Add point at current position
line.append(limitPoint(value.location))
startNewSegment = false
} else {
// Note: Now in mode where we are replacing the last point
line.removeLast()
.onEnded { value in
startNewSegment = true
func screenPoint(_ point: CGPoint) -> CGPoint {
// Convert 0->1 to view's coordinates
let vw = viewSize.width
let vh = viewSize.height
let nextX = min(1, max(0, point.x)) * vw
let nextY = min(1, max(0, point.y)) * vh
return CGPoint(x: nextX, y: nextY)
func limitPoint(_ point: CGPoint) -> CGPoint {
// Convert view coordinate to normalized 0->1 range
let vw = max(viewSize.width, 1)
let vh = max(viewSize.height, 1)
// Keep in bounds - even if dragging outside of bounds
let nextX = min(1, max(0, point.x / vw))
let nextY = min(1, max(0, point.y / vh))
func buttons() -> some View {
HStack {
Button {
line.removeAll()
pixelPath = ""
} label: {
Text("Clear")
// Show line points in "Pixel" units
if vw > 0 && vh > 0 {
let pixelWidth = CGFloat(pixelSize.width - 1)
let pixelHeight = CGFloat(pixelSize.height - 1)
let pixelPoints = line.map { CGPoint(x: pixelWidth * $0.x, y: pixelHeight * $0.y)}
pixelPath = "\(pixelPoints)"
Text("Pixel Path")
func info() -> some View {
Text("Image WxL: \(pixelSize.width) x \(pixelSize.height)")
Text("View WxL: \(viewSize.width) x \(viewSize.height)")
Text("Line points: \(line.count)")
if pixelPath != "" {
Text("Pixel Path: \(pixelPath)")
// Auxiliary definitions and functions
struct PixelSize {
var width: Int = 0
var height: Int = 0
extension UIImage {
var pixelSize: PixelSize {
if let cgImage = cgImage {
return PixelSize(width: cgImage.width, height: cgImage.height)
return PixelSize()
// SizeCalculator from: https://stackoverflow.com/questions/57577462/get-width-of-a-view-using-in-swiftui
struct SizeCalculator: ViewModifier {
@Binding var size: CGSize
func body(content: Content) -> some View {
content
.background(
GeometryReader { proxy in
Color.clear // we just want the reader to get triggered, so let's use an empty color
size = proxy.size
.onChange(of: proxy.size) { // Added to handle device rotation
extension View {
func saveSize(in size: Binding<CGSize>) -> some View {
modifier(SizeCalculator(size: size))
2条答案
按热度按时间d4so4syb1#
我建议你用
DragGesture
来记录点,用Canvas
来画线。您描述的路径上的点与捕获它们的位置不对应的问题是使用不同坐标空间的症状。您需要在捕获点的同一坐标空间中绘制线。例如:
DragGesture
连接到Image
,并在Image
上绘制重叠线DragGesture
连接到包含Image
的ZStack
,并在ZStack
内部的层中绘制线。下面是一个简单的实现,它在连续拖动手势的端点之间绘制直线。它使用了上述两种方法中的第二种,使用
ZStack
:字符串
的数据
这一解决办法可以以各种方式加以阐述,例如:
GeometryReader
获取拖动区域的大小,并将这些点转换为相对UnitPoint
位置,以便它们可以用于在已调整大小的图像上绘制相同的线nhaq1z212#
的数据
下面的代码让我们在图像的顶部绘制一条直线路径,如上图所示。直线的点被归一化到图像,这样即使在图像大小发生变化(例如设备旋转后可能发生的变化)后,也可以正确地重新绘制直线。
涉及的技巧包括在覆盖层中绘制线条。代码还显示了以像素为单位的图像大小和以点为单位的图像视图大小。这些大小在不同视图之间转换时很有用。
字符串