SwiftUI中如何实现由多段子动画组成的复杂自定义动画(以正负值切换进度条为例)
SwiftUI中如何实现由多段子动画组成的复杂自定义动画(以正负值切换进度条为例)
嘿,这个需求我刚好踩过坑!咱们先理清楚问题的核心:你原来的进度条只靠progress值的变化来驱动动画,但正负切换时(比如从+60到-60),progress的数值其实没变(都是60%),所以SwiftUI只会切换颜色,不会动宽度——这就是问题所在。要实现这种多段衔接的复杂动画,关键是把动画的控制逻辑从单一的数值变化,拆成可分步控制的状态流程。
下面我给你一套完整的实现方案,不仅能解决正负切换的动画问题,还能支持弹簧这类非线性动画,扩展性也强:
核心思路拆解
- 不要直接用外部传入的
currentValue驱动视图,而是内部维护一个displayedValue作为实际的显示值——这样咱们能手动控制它的动画过程 - 用一个状态标记
isAnimatingSwitch,防止快速操作时重复触发动画 - 当外部值变化时,判断是否需要正负切换:
- 同正负?直接动画过渡到目标值就行
- 要切换正负?先把显示值动画缩到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
相关产品推荐
相关产品推荐

