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
相关产品推荐
相关产品推荐

