如何取消可动画视图中视图属性的动画?

问题描述 投票:0回答:1

我有一个进度指示器视图,我希望能够从父视图中为当前进度级别设置动画。因此,按照我在所有示例中看到的相同模式,我将进度控件编写如下:

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()
    }
}

您会发现它不会立即更新可动画值的更改......但从逻辑上讲,我认为这是正确的。看起来一条曲线正在被加载到视图中以获得可设置动画的值,并且任何后续更改都会向曲线附加更多动画。合乎逻辑,但它削弱了该控件的预期功能。对于已公开可动画属性的视图,一般如何克服这一问题?

此处行为的屏幕截图

可在此处下载项目

animation swiftui
1个回答
0
投票

此行为是由 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
中,您只需进行一些简单的计算即可在给定目标值和当前时间的情况下找出可设置动画的值应该在哪里。这是您考虑动画之前是否被中断的地方。

© www.soinside.com 2019 - 2024. All rights reserved.