From 6f735b2d9a873c6d27431b8aef2f48d6a15a2f21 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 17 Aug 2025 23:13:57 +0800 Subject: [PATCH] Add VelocityTrackingAnimation --- .../Animation/VelocityTrackingAnimation.swift | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/Animation/Animation/VelocityTrackingAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/VelocityTrackingAnimation.swift index a16841f2d..4527b601e 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/VelocityTrackingAnimation.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/VelocityTrackingAnimation.swift @@ -2,16 +2,19 @@ // VelocityTrackingAnimation.swift // OpenSwiftUICore // -// Audited for iOS 18.0 -// Status: WIP +// Audited for 6.5.4 +// Status: Complete // ID: FD9125BC1E04E33D1D7BE4A31225AA98 (SwiftUICore) +import Foundation + // MARK: - TracksVelocityKey private struct TracksVelocityKey: TransactionKey { static var defaultValue: Bool { false } } +@available(OpenSwiftUI_v5_0, *) extension Transaction { /// Whether this transaction will track the velocity of any animatable /// properties that change. @@ -50,6 +53,68 @@ extension Transaction { } extension Animation { - // FIXME: VelocityTrackingAnimation - static let velocityTracking: Animation = .default + static let velocityTracking: Animation = Animation(VelocityTrackingAnimation()) +} + +private struct VelocityTrackingAnimation: CustomAnimation { + nonisolated func animate( + value: V, + time: TimeInterval, + context: inout AnimationContext + ) -> V? where V : VectorArithmetic { + var sampler = context.velocityState.sampler + if sampler.isEmpty { // FIXME: Verify this logic + sampler.addSample(value, time: .init(seconds: time)) + context.velocityState = .init(sampler: sampler) + } + let newTime = (sampler.lastTime?.seconds ?? .zero) + 2.0 + let velocity = velocity( + value: value, + time: time, + context: context + ) + if let velocity, velocity == .zero { + return nil + } + guard newTime > time else { + return nil + } + return value + } + + + nonisolated func velocity( + value: V, + time: TimeInterval, + context: AnimationContext + ) -> V? where V : VectorArithmetic { + let timeDiff = time - (context.velocityState.sampler.lastTime?.seconds ?? .zero) + let scale = pow(0.998, timeDiff * 1000) + return context.velocityState.sampler.velocity.scaled(by: scale).valuePerSecond + } + + nonisolated func shouldMerge( + previous: Animation, + value: V, + time: TimeInterval, + context: inout AnimationContext + ) -> Bool where V: VectorArithmetic { + context.velocityState.sampler.addSample(value, time: .init(seconds: time)) + return true + } +} + +extension AnimationContext { + fileprivate var velocityState: VelocityState { + get { state[VelocityState.self] } + set { state[VelocityState.self] = newValue } + } +} + +private struct VelocityState: AnimationStateKey where Value: VectorArithmetic { + static var defaultValue: VelocityState { + VelocityState(sampler: .init()) + } + + var sampler: VelocitySampler }