Ant*_*ney 1 xcode swift swift5 swiftui
似乎没有一种沿着自定义路径(尤其是弯曲路径)移动视图/形状的直观方法。我发现了几个 UIKit 库,它们允许视图在 Bézier 路径(DKChainableAnimationKit、TweenKit、Sica 等)上移动,但我不太习惯使用 UIKit 并不断遇到错误。
目前使用 swiftUI 我手动这样做:
import SwiftUI
struct ContentView: View {
@State var moveX = true
@State var moveY = true
@State var moveX2 = true
@State var moveY2 = true
@State var rotate1 = true
var body: some View {
ZStack{
Circle().frame(width:50, height:50)
.offset(x: moveX ? 0:100, y: moveY ? 0:100)
.animation(Animation.easeInOut(duration:1).delay(0))
.rotationEffect(.degrees(rotate1 ? 0:350))
.offset(x: moveX2 ? 0:-100, y: moveY2 ? 0:-200)
.animation(Animation.easeInOut(duration:1).delay(1))
.onAppear(){
self.moveX.toggle();
self.moveY.toggle();
self.moveX2.toggle();
self.moveY2.toggle();
self.rotate1.toggle();
// self..toggle()
}
}
} }
Run Code Online (Sandbox Code Playgroud)
它在某种程度上完成了工作,但灵活性受到严重限制,并且复合延迟很快就会变得一团糟。
如果有人知道我如何获得自定义视图/形状以沿着以下路径行进,将不胜感激。
Path { path in
path.move(to: CGPoint(x: 200, y: 100))
path.addQuadCurve(to: CGPoint(x: 230, y: 200), control: CGPoint(x: -100, y: 300))
path.addQuadCurve(to: CGPoint(x: 90, y: 400), control: CGPoint(x: 400, y: 130))
path.addLine(to: CGPoint(x: 90, y: 600))
}
.stroke()
Run Code Online (Sandbox Code Playgroud)
我设法找到的最接近的解决方案是在 SwiftUILab 上,但完整的教程似乎只适用于付费订阅者。
use*_*734 16
好吧,这并不简单,但我想帮助...
在下一个片段(macOS 应用程序)中,您可以看到可以根据需要进行调整的基本元素。
为简单起见,我选择简单的参数曲线,如果您喜欢使用更复杂的(复合)曲线,则必须解决如何将每个段的部分 t(参数)映射到整个曲线的复合 t(并且必须这样做)用于将部分沿轨道距离映射到复合轨道沿轨道距离)。
为什么会出现这样的并发症?
飞机位移(恒速)所需的沿航迹距离与参数曲线定义所依赖的曲线参数 t 之间存在非线性关系。
先来看看结果
接下来看看它是如何实现的。您需要研究此代码,并在必要时研究参数曲线的定义和行为方式。
//
// ContentView.swift
// tmp086
//
// Created by Ivo Vacek on 11/03/2020.
// Copyright © 2020 Ivo Vacek. All rights reserved.
//
import SwiftUI
import Accelerate
protocol ParametricCurve {
var totalArcLength: CGFloat { get }
func point(t: CGFloat)->CGPoint
func derivate(t: CGFloat)->CGVector
func secondDerivate(t: CGFloat)->CGVector
func arcLength(t: CGFloat)->CGFloat
func curvature(t: CGFloat)->CGFloat
}
extension ParametricCurve {
func arcLength(t: CGFloat)->CGFloat {
var tmin: CGFloat = .zero
var tmax: CGFloat = .zero
if t < .zero {
tmin = t
} else {
tmax = t
}
let quadrature = Quadrature(integrator: .qags(maxIntervals: 8), absoluteTolerance: 5.0e-2, relativeTolerance: 1.0e-3)
let result = quadrature.integrate(over: Double(tmin) ... Double(tmax)) { _t in
let dp = derivate(t: CGFloat(_t))
let ds = Double(hypot(dp.dx, dp.dy)) //* x
return ds
}
switch result {
case .success(let arcLength, _/*, let e*/):
//print(arcLength, e)
return t < .zero ? -CGFloat(arcLength) : CGFloat(arcLength)
case .failure(let error):
print("integration error:", error.errorDescription)
return CGFloat.nan
}
}
func curveParameter(arcLength: CGFloat)->CGFloat {
let maxLength = totalArcLength == .zero ? self.arcLength(t: 1) : totalArcLength
guard maxLength > 0 else { return 0 }
var iteration = 0
var guess: CGFloat = arcLength / maxLength
let maxIterations = 10
let maxErr: CGFloat = 0.1
while (iteration < maxIterations) {
let err = self.arcLength(t: guess) - arcLength
if abs(err) < maxErr { break }
let dp = derivate(t: guess)
let m = hypot(dp.dx, dp.dy)
guess -= err / m
iteration += 1
}
return guess
}
func curvature(t: CGFloat)->CGFloat {
/*
x'y" - y'x"
?(t) = --------------------
(x'² + y'²)^(3/2)
*/
let dp = derivate(t: t)
let dp2 = secondDerivate(t: t)
let dpSize = hypot(dp.dx, dp.dy)
let denominator = dpSize * dpSize * dpSize
let nominator = dp.dx * dp2.dy - dp.dy * dp2.dx
return nominator / denominator
}
}
struct Bezier3: ParametricCurve {
let p0: CGPoint
let p1: CGPoint
let p2: CGPoint
let p3: CGPoint
let A: CGFloat
let B: CGFloat
let C: CGFloat
let D: CGFloat
let E: CGFloat
let F: CGFloat
let G: CGFloat
let H: CGFloat
public private(set) var totalArcLength: CGFloat = .zero
init(from: CGPoint, to: CGPoint, control1: CGPoint, control2: CGPoint) {
p0 = from
p1 = control1
p2 = control2
p3 = to
A = to.x - 3 * control2.x + 3 * control1.x - from.x
B = 3 * control2.x - 6 * control1.x + 3 * from.x
C = 3 * control1.x - 3 * from.x
D = from.x
E = to.y - 3 * control2.y + 3 * control1.y - from.y
F = 3 * control2.y - 6 * control1.y + 3 * from.y
G = 3 * control1.y - 3 * from.y
H = from.y
// mandatory !!!
totalArcLength = arcLength(t: 1)
}
func point(t: CGFloat)->CGPoint {
let x = A * t * t * t + B * t * t + C * t + D
let y = E * t * t * t + F * t * t + G * t + H
return CGPoint(x: x, y: y)
}
func derivate(t: CGFloat)->CGVector {
let dx = 3 * A * t * t + 2 * B * t + C
let dy = 3 * E * t * t + 2 * F * t + G
return CGVector(dx: dx, dy: dy)
}
func secondDerivate(t: CGFloat)->CGVector {
let dx = 6 * A * t + 2 * B
let dy = 6 * E * t + 2 * F
return CGVector(dx: dx, dy: dy)
}
}
class AircraftModel: ObservableObject {
let track: ParametricCurve
let path: Path
var aircraft: some View {
let t = track.curveParameter(arcLength: alongTrackDistance)
let p = track.point(t: t)
let dp = track.derivate(t: t)
let h = Angle(radians: atan2(Double(dp.dy), Double(dp.dx)))
return Text("").font(.largeTitle).rotationEffect(h).position(p)
}
@Published var alongTrackDistance = CGFloat.zero
init(from: CGPoint, to: CGPoint, control1: CGPoint, control2: CGPoint) {
track = Bezier3(from: from, to: to, control1: control1, control2: control2)
path = Path({ (path) in
path.move(to: from)
path.addCurve(to: to, control1: control1, control2: control2)
})
}
}
struct ContentView: View {
@ObservedObject var aircraft = AircraftModel(from: .init(x: 0, y: 0), to: .init(x: 500, y: 600), control1: .init(x: 600, y: 100), control2: .init(x: -300, y: 400))
var body: some View {
VStack {
ZStack {
aircraft.path.stroke(style: StrokeStyle( lineWidth: 0.5))
aircraft.aircraft
}
Slider(value: $aircraft.alongTrackDistance, in: (0.0 ... aircraft.track.totalArcLength)) {
Text("along track distance")
}.padding()
Button(action: {
// fly (to be implemented :-))
}) {
Text("Fly!")
}.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Run Code Online (Sandbox Code Playgroud)
如果您担心如何实现“动画”飞机运动,SwiftUI 动画不是解决方案。您必须以编程方式移动飞机。
你必须导入
import Combine
Run Code Online (Sandbox Code Playgroud)
添加到模型
@Published var flying = false
var timer: Cancellable? = nil
func fly() {
flying = true
timer = Timer
.publish(every: 0.02, on: RunLoop.main, in: RunLoop.Mode.default)
.autoconnect()
.sink(receiveValue: { (_) in
self.alongTrackDistance += self.track.totalArcLength / 200.0
if self.alongTrackDistance > self.track.totalArcLength {
self.timer?.cancel()
self.flying = false
}
})
}
Run Code Online (Sandbox Code Playgroud)
并修改按钮
Button(action: {
self.aircraft.fly()
}) {
Text("Fly!")
}.disabled(aircraft.flying)
.padding()
Run Code Online (Sandbox Code Playgroud)
最后我有
user3441734 的解决方案非常通用和优雅。读者将受益于对它ParametricCurve及其弧长和曲率的每一秒思考。这是我发现的唯一可以将移动物体(飞机)重新定位为在移动时指向前方的方法。
Asperi 还在Is it possible to animate view on a certain Path in SwiftUI中发布了一个有用的解决方案
这是一个事半功倍、事半功倍的解决方案。它确实使用了 SwiftUI 动画,这是喜忧参半的。(例如,您可以对动画曲线有更多选择,但动画完成后您不会收到公告或回调。)它的灵感来自 Asperi 在Problem animating with animatableData in SwiftUI中的答案。
import SwiftUI
// Use https://www.desmos.com/calculator/cahqdxeshd to design Beziers.
// Pick a simple example path.
fileprivate let W = UIScreen.main.bounds.width
fileprivate let H = UIScreen.main.bounds.height
fileprivate let p1 = CGPoint(x: 50, y: H - 50)
fileprivate let p2 = CGPoint(x: W - 50, y: 50)
fileprivate var samplePath : Path {
let c1 = CGPoint(x: p1.x, y: (p1.y + p2.y)/2)
let c2 = CGPoint(x: p2.x, y: (p1.y + p2.y)/2)
var result = Path()
result.move(to: p1)
result.addCurve(to: p2, control1: c1, control2: c2)
return result
}
// This View's position follows the Path.
struct SlidingSpot : View {
let path : Path
let start : CGPoint
let duration: Double = 1
@State var isMovingForward = false
var tMax : CGFloat { isMovingForward ? 1 : 0 } // Same expressions,
var opac : Double { isMovingForward ? 1 : 0 } // different meanings.
var body: some View {
VStack {
Circle()
.frame(width: 30)
// Asperi is correct that this Modifier must be separate.
.modifier(Moving(time: tMax, path: path, start: start))
.animation(.easeInOut(duration: duration), value: tMax)
.opacity(opac)
Button {
isMovingForward = true
// Sneak back to p1. This is a code smell.
DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.1) {
isMovingForward = false
}
} label: {
Text("Go")
}
}
}
}
// Minimal modifier.
struct Moving: AnimatableModifier {
var time : CGFloat // Normalized from 0...1.
let path : Path
let start: CGPoint // Could derive from path.
var animatableData: CGFloat {
get { time }
set { time = newValue }
}
func body(content: Content) -> some View {
content
.position(
path.trimmedPath(from: 0, to: time).currentPoint ?? start
)
}
}
struct ContentView: View {
var body: some View {
SlidingSpot(path: samplePath, start: p1)
}
}
Run Code Online (Sandbox Code Playgroud)
尝试这个:
但是:请小心:这不是在预览中运行,您必须在模拟器/设备上运行
struct MyShape: Shape {
func path(in rect: CGRect) -> Path {
let path =
Path { path in
path.move(to: CGPoint(x: 200, y: 100))
path.addQuadCurve(to: CGPoint(x: 230, y: 200), control: CGPoint(x: -100, y: 300))
path.addQuadCurve(to: CGPoint(x: 90, y: 400), control: CGPoint(x: 400, y: 130))
path.addLine(to: CGPoint(x: 90, y: 600))
}
return path
}
}
struct ContentView: View {
@State var x: CGFloat = 0.0
var body: some View {
MyShape()
.trim(from: 0, to: x)
.stroke(lineWidth: 10)
.frame(width: 200, height: 200)
.onAppear() {
withAnimation(Animation.easeInOut(duration: 3).delay(0.5)) {
self.x = 1
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
2970 次 |
| 最近记录: |