如何在 SwiftUI 中自定义角度变化的动画
Posted
技术标签:
【中文标题】如何在 SwiftUI 中自定义角度变化的动画【英文标题】:How can I customise the Animation of an Angle change in SwiftUI 【发布时间】:2020-01-30 15:01:26 【问题描述】:我有一个应用程序,它显示了一群人,每个人都有一个起源和角度。
struct Location
var centre:CGPoint
var facing:Angle
当它们从位置 A 移动到位置 B 时,SwiftUI 会神奇地自动执行许多动画
withAnimation
person.location = newLocation
但是 - 对于角度(面对)属性,我希望动画以最短的路线进行(请记住,在现实世界中 - 角度环绕)。
例如当角度变化 5 -> 10(度)时,Swift UI 会正确动画
5,6,7,8,9,10
但是从 2 到 358,需要很长的路要走
SwiftUI 做了 2,3,4,5,6,7.......,357,358
我希望它在哪里做
2,1,0,359,358
我该怎么办?
谢谢
更新:我希望有一个解决方案,可以让我使用动画系统,也许使用一个新的 MyAngle 结构,它直接提供动画步骤,也许使用某种动画修改器。 .easeInOut 修改了步骤 - 是否有等效的方法可以创建 .goTheRightWay 动画?
【问题讨论】:
【参考方案1】:好的 - 发布我自己的答案。 它有点像@Ben 的回答 - 但将“阴影角度”管理移至旋转效果。
您只需将rotationEffect(angle:Angle)
切换为shortRotationEffect(angle:Angle,id:UUID)
这看起来像
Image(systemName: "person.fill").resizable()
.frame(width: 50, height: 50)
.shortRotationEffect(self.person.angle,id:person.id)
.animation(.easeInOut)
ShortRotationEffect 使用提供的 id 来维护先前角度的字典。当您设置一个新角度时,它会计算出提供短旋转的等效角度并将其应用于普通rotationEffect(...)
这里是:
extension View
/// Like RotationEffect - but when animated, the rotation moves in the shortest direction.
/// - Parameters:
/// - angle: new angle
/// - anchor: anchor point
/// - id: unique id for the item being displayed. This is used as a key to maintain the rotation history and figure out the right direction to move
func shortRotationEffect(_ angle: Angle,anchor: UnitPoint = .center, id:UUID) -> some View
modifier(ShortRotation(angle: angle,anchor:anchor, id:id))
struct ShortRotation: ViewModifier
static var storage:[UUID:Angle] = [:]
var angle:Angle
var anchor:UnitPoint
var id:UUID
func getAngle() -> Angle
var newAngle = angle
if let lastAngle = ShortRotation.storage[id]
let change:Double = (newAngle.degrees - lastAngle.degrees) %% 360.double
if change < 180
newAngle = lastAngle + Angle.init(degrees: change)
else
newAngle = lastAngle + Angle.init(degrees: change - 360)
ShortRotation.storage[id] = newAngle
return newAngle
func body(content: Content) -> some View
content
.rotationEffect(getAngle(),anchor: anchor)
这取决于我的正模函数:
public extension Double
/// Returns modulus, but forces it to be positive
/// - Parameters:
/// - left: number
/// - right: modulus
/// - Returns: positive modulus
static func %% (_ left: Double, _ right: Double) -> Double
let truncatingRemainder = left.truncatingRemainder(dividingBy: right)
return truncatingRemainder >= 0 ? truncatingRemainder : truncatingRemainder+abs(right)
【讨论】:
这更好,但不太奏效。当我旋转一个完整的圆圈时,它仍然会在 360 度旋转结束时跳跃。不过,这确实解决了 0 到 360 之间发生的跳跃。【参考方案2】:如何调整 newLocation 值以保持在开始的 180˚ 以内?这是一个检查动画距离是否大于一半并提供满足它的新端点的函数。
func adjustedEnd(from start: CGFloat, to target: CGFloat) -> CGFloat
// Shift end to be greater than start
var end = target
while end < start end += 360
// Mod the distance with 360, shifting by 180 to keep on the same side of a circle
return (end - start + 180).truncatingRemainder(dividingBy: 360) - 180 + start
一些示例测试用例:
let startValues: [CGFloat] = [2, -10, 345, 365, 700]
let endValues: [CGFloat] = [2, 10, 180, 185, 350, -10, 715, -700]
for start in startValues
print("From \(start):")
for end in endValues
let adjusted = adjustedEnd(from: start, to: end)
print("\t\(end) \tbecomes \(adjusted);\tdistance \(abs(adjusted - start))")
打印以下内容:
From 2.0:
2.0 becomes 2.0; distance 0.0
10.0 becomes 10.0; distance 8.0
180.0 becomes 180.0; distance 178.0
185.0 becomes -175.0; distance 177.0
350.0 becomes -10.0; distance 12.0
-10.0 becomes -10.0; distance 12.0
715.0 becomes -5.0; distance 7.0
-700.0 becomes 20.0; distance 18.0
From -10.0:
2.0 becomes 2.0; distance 12.0
10.0 becomes 10.0; distance 20.0
180.0 becomes -180.0; distance 170.0
185.0 becomes -175.0; distance 165.0
350.0 becomes -10.0; distance 0.0
-10.0 becomes -10.0; distance 0.0
715.0 becomes -5.0; distance 5.0
-700.0 becomes 20.0; distance 30.0
From 345.0:
2.0 becomes 362.0; distance 17.0
10.0 becomes 370.0; distance 25.0
180.0 becomes 180.0; distance 165.0
185.0 becomes 185.0; distance 160.0
350.0 becomes 350.0; distance 5.0
-10.0 becomes 350.0; distance 5.0
715.0 becomes 355.0; distance 10.0
-700.0 becomes 380.0; distance 35.0
From 365.0:
2.0 becomes 362.0; distance 3.0
10.0 becomes 370.0; distance 5.0
180.0 becomes 540.0; distance 175.0
185.0 becomes 185.0; distance 180.0
350.0 becomes 350.0; distance 15.0
-10.0 becomes 350.0; distance 15.0
715.0 becomes 355.0; distance 10.0
-700.0 becomes 380.0; distance 15.0
From 700.0:
2.0 becomes 722.0; distance 22.0
10.0 becomes 730.0; distance 30.0
180.0 becomes 540.0; distance 160.0
185.0 becomes 545.0; distance 155.0
350.0 becomes 710.0; distance 10.0
-10.0 becomes 710.0; distance 10.0
715.0 becomes 715.0; distance 15.0
-700.0 becomes 740.0; distance 40.0
(已编辑以考虑负结束值)
编辑:根据您关于保留第二个值的评论,如何将 Location.facing
设置为调整后的角度,然后添加到 Location 类似
var prettyFacing: Angle
var facing = self.facing
while facing.degrees < 0 facing += Angle(degrees: 360)
while facing.degrees > 360 facing -= Angle(degrees: 360)
return facing
【讨论】:
感谢您的加入。我已经考虑过这种方法 - 但它需要维护一组阴影位置,当您更改实际位置时必须调整这些位置。我希望有一种方法可以直接更改动画逻辑。在动画系统中的某个地方 - 必须有一些东西正在弄清楚 A 和 B 之间的步骤...... 啊,我看到了@ConfusedVorlon。人物模型是否需要将角度值保持在 0 到 360 之间?如果 location.facing 被设置为这个调整后的值,然后你向Location
添加了一个计算变量,比如prettyFacing
,如果必要的话将facing
调整回非负数?
答案已编辑为示例。我意识到的动画问题仍然没有帮助,只是想也许它可能会有所帮助!【参考方案3】:
在尝试了其他两个选项后,我们仍然遇到视觉故障(不太常见,但仍然存在!)。
我们的解决方案:使用 UIKit 制作动画
我们创建了一个 SPM 包,它添加了一个简单的修饰符 .uiRotationEffect()
。此修饰符将您的View
包装在UIView
中,并使用UIView
的.animate(...)
函数来获得正确的行为。
你可以安装包here或者直接复制粘贴代码here,不会很长。
工作解决方案的 GIF:
【讨论】:
以上是关于如何在 SwiftUI 中自定义角度变化的动画的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI:如何在 MapKit 中自定义地理区域的位置?
SwiftUI ButtonStyle scaleEffect 动画按钮位置变化