我有一个进度指示器视图,我希望能够从父视图中为当前进度级别设置动画。因此,按照我在所有示例中看到的相同模式,我将进度控件编写如下:
import SwiftUI
struct CircularProgressView: View, Animatable
{
var percentComplete: Float
@State var FGColor: Color
@State var BGColor: Color
var animatableData: Float
{
get
{
return percentComplete
}
set
{
percentComplete = newValue
}
}
init(foregroundColor: Color, backgroundColor: Color = Color.clear, percentComplete: Float)
{
FGColor = foregroundColor
BGColor = backgroundColor
self.percentComplete = percentComplete
print("Progress control init with \(percentComplete) %")
}
var body: some View
{
Text("progressControl internal percent: \(percentComplete)")
ZStack
{
Circle()
.stroke(
BGColor.opacity(0.5),
lineWidth: 30
)
Circle()
.trim(from: 0, to: CGFloat(percentComplete / 100))
.stroke(
FGColor,
lineWidth: 30
)
.rotationEffect(.degrees(-90))
}
}
}
如果将其放入另一个视图并尝试按如下方式使用其动画进度:
import SwiftUI
struct ContentView: View
{
@State var progressStarted: Bool = false
var body: some View
{
VStack
{
CircularProgressView(foregroundColor: Color.red, percentComplete: progressStarted ? 100 : 0)
.animation(.linear(duration: 5), value: progressStarted)
Text("Progress is \(progressStarted ? "started" : "stopped")")
Button(action: {progressStarted.toggle()}, label: {
Text("Toggle progress")
})
}
.padding()
}
}
您会发现它不会立即更新可动画值的更改......但从逻辑上讲,我认为这是正确的。看起来一条曲线正在被加载到视图中以获得可设置动画的值,并且任何后续更改都会向曲线附加更多动画。合乎逻辑,但它削弱了该控件的预期功能。对于已公开可动画属性的视图,一般如何克服这一问题?
此行为是由 SwiftUI 同时运行两个动画(0 到 100 和 100 到 0)引起的。 SwiftUI 以自己的方式将这些结合起来,并且两个动画运行的周期都是非线性的。这是默认行为。
要覆盖这一点,我认为您需要创建自己的
CustomAnimation
。假设您想以恒定速度为进度条设置动画,每 100(完整)进度 5 秒,您可以这样做:
// adapted from https://swiftui-lab.com/swiftui-animations-part6/
struct MyLinearState<Value: VectorArithmetic>: AnimationStateKey {
var from: Value? = nil
var interruption: TimeInterval? = nil
static var defaultValue: Self { MyLinearState() }
}
extension AnimationContext {
var myLinearState: MyLinearState<Value> {
get { state[MyLinearState<Value>.self] }
set { state[MyLinearState<Value>.self] = newValue }
}
}
struct MyLinearAnimation: CustomAnimation {
let speed: TimeInterval
func animate<V: VectorArithmetic>(
value: V, time: TimeInterval, context: inout AnimationContext<V>
) -> V? {
let distance = sqrt((value - (context.myLinearState.from ?? .zero)).magnitudeSquared)
let duration = speed * distance
guard time < duration + (context.myLinearState.interruption ?? 0) else { return nil }
if let from = context.myLinearState.from {
return from.interpolated(towards: value, amount: (time-context.myLinearState.interruption!)/duration)
} else {
return value.scaled(by: time/duration)
}
}
func shouldMerge<V>(previous: Animation, value: V, time: TimeInterval, context: inout AnimationContext<V>) -> Bool where V : VectorArithmetic {
context.myLinearState.from = previous.base.animate(value: value, time: time, context: &context)
context.myLinearState.interruption = time
return true
}
}
// the animation modifier:
.animation(.init(MyLinearAnimation(speed: 5 / 100)), value: progressStarted)
至关重要的是,在
true
中返回 shouldMerge
,这样 SwiftUI 就不会同时运行两个动画。您想将两个动画合并为一个。
在
animate
中,您只需进行一些简单的计算即可在给定目标值和当前时间的情况下找出可设置动画的值应该在哪里。这是您考虑动画之前是否被中断的地方。