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

Spring动画场景下,如何确保两个视图对齐且同步执行动画?

Apple Music滑块展开动画不同步问题修复

问题描述

我正在复刻Apple Music里的滑块展开动画,快速点击(点击后立即松开)时,矩形的展开动画和时间标签的展开动画不同步——明明逻辑上用的是相同的动画,视觉上却像是两种不同的弹簧动画。怀疑是easeInOut动画在手指松开后未完成,导致后续Spring动画接管前出现异常延迟。

原代码如下:

struct SliderExpansionTest: View {
    @State private var isDraggedOn = false
    
    private let expansion = 1.08
    
    var body: some View {
        VStack {
            Rectangle()
                .cornerRadius(35)
                .frame(width: isDraggedOn ? 300 * expansion : 300)
                .frame(height: isDraggedOn ? 20 : 10)
                .gesture(
                    DragGesture(minimumDistance: 0)
                        .onChanged { _ in
                            withAnimation(.easeInOut) {
                                isDraggedOn = true
                            }
                        }
                        .onEnded { _ in
                            withAnimation(.spring(response: 0.4, dampingFraction: 0.6, blendDuration: 0)) {
                                isDraggedOn = false
                            }
                        }
                )
            
            TimeLabel(isDraggedOn: $isDraggedOn)
        }
        .frame(width: isDraggedOn ? 300 * expansion : 300)
    }
}

struct TimeLabel: View {
    @Binding var isDraggedOn: Bool
    
    var body: some View {
        HStack {
            Text("00:00")
                .fontWeight(.bold)
                .fontDesign(.rounded)
                .foregroundColor(isDraggedOn ? .white : .secondary)
                .font(.system(size: isDraggedOn ? 11 : 10))
            
            Spacer()

            Text("-00:00")
                .fontWeight(.bold)
                .fontDesign(.rounded)
                .foregroundColor(isDraggedOn ? .white : .secondary)
                .font(.system(size: isDraggedOn ? 11 : 10))
        }
    }
}

问题根源

原代码在手势回调的onChanged和onEnded中,分别用withAnimation包裹状态变化。当快速点击时,onChanged触发的easeInOut动画尚未完成,onEnded的spring动画就已启动,两种动画叠加导致不同视图的动画进度不一致,视觉上出现错位。

解决方案

方案1:统一动画触发逻辑

将动画绑定到状态变量上,通过.animation(_:value:)修饰符统一控制所有依赖该状态的视图动画,避免多动画冲突。

修改后的代码:

struct SliderExpansionTest: View {
    @State private var isDraggedOn = false
    
    private let expansion = 1.08
    
    var body: some View {
        VStack {
            Rectangle()
                .cornerRadius(35)
                .frame(width: isDraggedOn ? 300 * expansion : 300)
                .frame(height: isDraggedOn ? 20 : 10)
                .gesture(
                    DragGesture(minimumDistance: 0)
                        .onChanged { _ in
                            isDraggedOn = true
                        }
                        .onEnded { _ in
                            isDraggedOn = false
                        }
                )
            
            TimeLabel(isDraggedOn: $isDraggedOn)
        }
        .frame(width: isDraggedOn ? 300 * expansion : 300)
        // 根据状态值切换对应动画,所有关联视图同步执行
        .animation(isDraggedOn ? .easeInOut : .spring(response: 0.4, dampingFraction: 0.6, blendDuration: 0), value: isDraggedOn)
    }
}

struct TimeLabel: View {
    @Binding var isDraggedOn: Bool
    
    var body: some View {
        HStack {
            Text("00:00")
                .fontWeight(.bold)
                .fontDesign(.rounded)
                .foregroundColor(isDraggedOn ? .white : .secondary)
                .font(.system(size: isDraggedOn ? 11 : 10))
            
            Spacer()

            Text("-00:00")
                .fontWeight(.bold)
                .fontDesign(.rounded)
                .foregroundColor(isDraggedOn ? .white : .secondary)
                .font(.system(size: isDraggedOn ? 11 : 10))
        }
    }
}

方案2:优化手势状态管理(更贴合Apple Music交互)

使用@GestureState处理拖拽时的临时状态,分离拖拽与点击的逻辑,动画统一由复合状态控制,交互更精准。

修改后的代码:

struct SliderExpansionTest: View {
    @State private var isExpanded = false
    @GestureState private var isDragging = false
    
    private let expansion = 1.08
    
    var body: some View {
        VStack {
            Rectangle()
                .cornerRadius(35)
                .frame(width: isDragging || isExpanded ? 300 * expansion : 300)
                .frame(height: isDragging || isExpanded ? 20 : 10)
                .gesture(
                    DragGesture(minimumDistance: 0)
                        .updating($isDragging) { _, state, _ in
                            state = true
                        }
                        .onEnded { _ in
                            isExpanded = false
                        }
                )
                // 新增点击切换展开/收起逻辑
                .onTapGesture {
                    isExpanded.toggle()
                }
            
            TimeLabel(isDraggedOn: .constant(isDragging || isExpanded))
        }
        .frame(width: isDragging || isExpanded ? 300 * expansion : 300)
        .animation(isDragging || isExpanded ? .easeInOut : .spring(response: 0.4, dampingFraction: 0.6, blendDuration: 0), value: isDragging || isExpanded)
    }
}

struct TimeLabel: View {
    @Binding var isDraggedOn: Bool
    
    var body: some View {
        HStack {
            Text("00:00")
                .fontWeight(.bold)
                .fontDesign(.rounded)
                .foregroundColor(isDraggedOn ? .white : .secondary)
                .font(.system(size: isDraggedOn ? 11 : 10))
            
            Spacer()

            Text("-00:00")
                .fontWeight(.bold)
                .fontDesign(.rounded)
                .foregroundColor(isDraggedOn ? .white : .secondary)
                .font(.system(size: isDraggedOn ? 11 : 10))
        }
    }
}

效果说明

两种方案都能确保矩形、时间标签和外层frame的动画完全同步,快速点击时不会出现延迟或错位问题。方案2额外支持点击切换展开状态,更接近Apple Music的原生交互体验。

内容的提问来源于stack exchange,提问作者User95797654974

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 09:15:10