SwiftUI中如何为路径设置Double类型上下界以定位视图?
实现带数值范围的路径跟随视图
当然可以实现!核心思路是把路径的总长度映射到你设定的lowerBound到upperBound数值范围,然后根据currentValue计算出对应的路径百分比,再获取该百分比位置的坐标和切线角度,就能定位并旋转任意SwiftUI视图。
关键步骤拆解
计算路径总长度
先通过Path的cgPath获取总长度,这是数值范围和路径位置映射的基础:extension Path { var totalLength: CGFloat { var length: CGFloat = 0 let path = cgPath path.applyWithBlock { element in guard let points = element.pointee.points else { return } switch element.pointee.type { case .moveToPoint: break case .addLineToPoint: length += hypot(points[0].x - points[1].x, points[0].y - points[1].y) case .addQuadCurveToPoint: // 贝塞尔曲线近似长度计算,也可用更精确的分段计算 let start = points[0], control = points[1], end = points[2] let a = hypot(start.x - 2*control.x + end.x, start.y - 2*control.y + end.y) let b = hypot(2*control.x - 2*start.x, 2*control.y - 2*start.y) length += (a + b)/2 case .addCurveToPoint: // 三次贝塞尔曲线同理,用近似或分段计算 let start = points[0], c1 = points[1], c2 = points[2], end = points[3] let a = hypot(start.x - 3*c1.x + 3*c2.x - end.x, start.y - 3*c1.y + 3*c2.y - end.y) let b = hypot(3*c1.x - 3*start.x, 3*c1.y - 3*start.y) let c = hypot(3*c2.x - 3*c1.x, 3*c2.y - 3*c1.y) length += (a + b + c)/3 case .closeSubpath: break @unknown default: break } } return length } }根据数值计算路径上的位置
把currentValue归一化到0-1的百分比,再通过CGPath的截断方法获取对应位置的坐标:func point(on path: Path, at percentage: CGFloat) -> CGPoint { let clampedPercentage = min(max(percentage, 0), 1) let trimmedPath = path.cgPath.copy(trimmedFrom: 0, to: clampedPercentage) ?? path.cgPath var endPoint = CGPoint.zero trimmedPath.applyWithBlock { element in switch element.pointee.type { case .moveToPoint, .addLineToPoint: endPoint = element.pointee.points![0] case .addQuadCurveToPoint: endPoint = element.pointee.points![2] case .addCurveToPoint: endPoint = element.pointee.points![3] default: break } } return endPoint }获取路径切线角度(可选,用于视图旋转)
如果需要视图沿路径方向自动旋转,可以计算对应位置的切线角度:func angle(on path: Path, at percentage: CGFloat) -> Angle { let clampedPercentage = min(max(percentage, 0), 1) let delta = 0.001 // 取极短一段路径计算切线 let startPercent = max(clampedPercentage - delta, 0) let endPercent = min(clampedPercentage + delta, 1) let startPoint = point(on: path, at: startPercent) let endPoint = point(on: path, at: endPercent) let dx = endPoint.x - startPoint.x let dy = endPoint.y - startPoint.y let angle = atan2(dy, dx) return Angle(radians: Double(angle)) }封装成可复用的
PathFollowView
把逻辑封装成通用视图,支持自定义路径、数值范围、当前值和跟随视图:struct PathFollowView<Content: View>: View { let path: Path let lowerBound: CGFloat let upperBound: CGFloat let currentValue: CGFloat let content: () -> Content private var percentage: CGFloat { guard upperBound != lowerBound else { return 0 } let normalizedValue = currentValue - lowerBound let range = upperBound - lowerBound return normalizedValue / range } var body: some View { ZStack { path.stroke() // 可选:显示路径作为参考 content() .position(point(on: path, at: percentage)) .rotationEffect(angle(on: path, at: percentage)) } } }
使用示例
struct ContentView: View { @State private var currentValue = 25.0 var body: some View { PathFollowView( path: makeCustomPath(), lowerBound: 0, upperBound: 100, currentValue: currentValue, content: { Image(systemName: "car.fill") .foregroundColor(.red) .font(.title) } ) .onAppear { // 测试动画:手动修改currentValue即可触发位置更新,无持续动画问题 withAnimation(.easeInOut(duration: 3)) { currentValue = 100 } } } func makeCustomPath() -> Path { var path = Path() path.move(to: CGPoint(x: 50, y: 50)) path.addQuadCurve(to: CGPoint(x: 300, y: 50), control: CGPoint(x: 175, y: 150)) path.addLine(to: CGPoint(x: 300, y: 250)) path.addCurve(to: CGPoint(x: 50, y: 250), control1: CGPoint(x: 225, y: 350), control2: CGPoint(x: 125, y: 350)) return path } }
解决持续动画问题
之前的持续动画问题,大概率是因为未正确截断路径或依赖了自动触发的状态更新。上面的实现中,percentage是纯计算属性,只有currentValue变化时才会更新位置,不会产生无意义的持续动画。
内容的提问来源于stack exchange,提问作者Takasur
相关产品推荐
相关产品推荐

