You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中如何实现由多段子动画组成的复杂自定义动画(以正负值切换进度条为例)

SwiftUI中如何实现由多段子动画组成的复杂自定义动画(以正负值切换进度条为例)

嘿,这个需求我刚好踩过坑!咱们先理清楚问题的核心:你原来的进度条只靠progress值的变化来驱动动画,但正负切换时(比如从+60到-60),progress的数值其实没变(都是60%),所以SwiftUI只会切换颜色,不会动宽度——这就是问题所在。要实现这种多段衔接的复杂动画,关键是把动画的控制逻辑从单一的数值变化,拆成可分步控制的状态流程。

下面我给你一套完整的实现方案,不仅能解决正负切换的动画问题,还能支持弹簧这类非线性动画,扩展性也强:

核心思路拆解

  1. 不要直接用外部传入的currentValue驱动视图,而是内部维护一个displayedValue作为实际的显示值——这样咱们能手动控制它的动画过程
  2. 用一个状态标记isAnimatingSwitch,防止快速操作时重复触发动画
  3. 当外部值变化时,判断是否需要正负切换:
    • 同正负?直接动画过渡到目标值就行
    • 要切换正负?先把显示值动画缩到0,等这段动画结束,再接着动画展开到目标值,而且两段动画的总时长固定为1秒

完整代码实现

struct AnimatedProgressBar: View {
    var height: CGFloat = 20
    var minValue: Double = 0
    var maxValue: Double = 100
    
    @Binding var currentValue: Double
    @State private var displayedValue: Double = 0
    @State private var isAnimatingSwitch = false
    
    // 固定总动画时长
    private let totalAnimationDuration: TimeInterval = 1.0
    
    var body: some View {
        let maxV = max(minValue, maxValue)
        let minV = min(minValue, maxValue)
        let isDisplayedNegative = displayedValue < minV
        // 计算当前显示值对应的进度比例
        let progress = abs(displayedValue - minV) / (maxV - minV)
        
        Rectangle()
            .fill(.gray)
            .frame(height: height)
            .overlay(alignment: .leading) {
                GeometryReader { geometry in
                    Rectangle()
                        .fill(isDisplayedNegative ? .red : .green)
                        .frame(width: geometry.size.width * progress)
                }
            }
            .onAppear {
                // 初始化显示值,避免初始状态跳变
                displayedValue = currentValue
            }
            .onChange(of: currentValue) { newValue in
                let isCurrentNegative = displayedValue < minV
                let isNewNegative = newValue < minV
                
                if isCurrentNegative == isNewNegative {
                    // 同正负,直接动画到目标值
                    withAnimation(.easeInOut(duration: totalAnimationDuration)) {
                        displayedValue = newValue
                    }
                } else {
                    // 正负切换,分两段动画执行
                    guard !isAnimatingSwitch else { return }
                    isAnimatingSwitch = true
                    
                    // 计算两段动画的时长比例:按当前值和目标值的绝对值占比分配总时长
                    let currentAbs = abs(displayedValue - minV)
                    let newAbs = abs(newValue - minV)
                    let firstDuration = totalAnimationDuration * (currentAbs / (currentAbs + newAbs))
                    let secondDuration = totalAnimationDuration - firstDuration
                    
                    // 第一段:缩到0
                    withAnimation(.easeInOut(duration: firstDuration)) {
                        displayedValue = minV // 对应进度为0的基准值
                    } completion: {
                        // 第一段结束后,执行第二段:展开到目标值
                        withAnimation(.easeInOut(duration: secondDuration)) {
                            displayedValue = newValue
                        } completion: {
                            // 动画全部完成,重置标记
                            isAnimatingSwitch = false
                        }
                    }
                }
            }
    }
}

// 测试用视图
struct AnimatedProgressBarTestView: View {
    @State var value: Double = 40
    
    var body: some View {
        VStack(spacing: 20) {
            AnimatedProgressBar(currentValue: $value)
            
            Button("随机切换值") {
                value = .random(in: -100...100)
            }
            .padding()
            .background(.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
        }
        .padding()
    }
}

关键细节说明

  • 动画衔接:这里用了iOS 15+新增的withAnimation(_:completion:)API,能精准监听第一段动画的结束,无缝触发第二段——如果要兼容更低版本,可以自定义一个AnimationCompletionModifier来实现同样的回调效果
  • 非线性动画支持:如果想换成弹簧这类非线性动画,只需要把.easeInOut(duration:)换成.spring(response: 0.5, dampingFraction: 0.7)就行,两段动画的曲线会自然衔接,不会出现卡顿或断层
  • 防重复触发:isAnimatingSwitch标记能避免快速点击按钮时,多个动画叠加导致的混乱

扩展思路(更复杂的动画场景)

如果以后需要实现三段甚至更多段的复杂动画,咱们可以用状态机来管理:比如定义一个枚举AnimationState,包含.idle、.shrinking、.expanding、.colorFading等状态,然后在每个状态的动画完成后,切换到下一个状态,触发对应的动画逻辑。这种方式能把复杂的动画流程拆解得更清晰,维护起来也更方便。

备注:内容来源于stack exchange,提问作者Andrei Herford

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 10:03:04