自定义SegmentedControl流体动画实现方案求助
实现带拉伸效果的SegmentedControl指示器动画
你的现有方案是给每个选项单独设置圆形背景,切换时只是各自的显隐动画,没法实现连贯的拉伸移动效果。要做这种动画,得把选中的白色指示器做成独立的可移动视图,通过位置偏移和缩放组合来实现先拉伸后恢复的效果。
下面是具体实现代码,我会逐部分解释:
import SwiftUI // 用于收集每个选项的位置信息 private struct SegmentPreferenceKey: PreferenceKey { static var defaultValue: [RotationCase: CGRect] = [:] static func reduce(value: inout [RotationCase: CGRect], nextValue: () -> [RotationCase: CGRect]) { value.merge(nextValue()) { $1 } } } struct SegmentedControl: View { @State var position: RotationCase = .P1 // 存储每个选项的位置信息 @State private var segmentFrames: [RotationCase: CGRect] = [:] var body: some View { ZStack(alignment: .leading) { // 白色指示器:单独的视图,负责移动和拉伸动画 RoundedRectangle(cornerRadius: 22.5) // 和容器Capsule圆角匹配 .foregroundColor(.white) .frame(width: segmentFrames[position]?.width ?? 0, height: 45) .offset(x: segmentFrames[position]?.minX ?? 0) .animation( Animation.easeInOut(duration: 0.3) .delay(0.05), value: position ) .scaleEffect(x: isAnimating ? 1.2 : 1.0, y: 1.0) .animation( Animation.easeInOut(duration: 0.2) .repeatCount(2, autoreverses: true), value: isAnimating ) HStack(spacing: 0) { ForEach(RotationCase.allCases, id: \.self) { rcase in Text(rcase.rawValue) .foregroundColor(rcase == position ? .black : .white) .padding() .background(Color.clear) // 去掉原来的背景,让指示器在底层显示 .onTapGesture { isAnimating = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { withAnimation { position = rcase } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { isAnimating = false } } } // 收集每个选项的位置信息 .background( GeometryReader { geo in Color.clear .preference( key: SegmentPreferenceKey.self, value: [rcase: geo.frame(in: .named("SegmentContainer"))] ) } ) } } } .frame(height: 45) .padding(10) .background(.black) .clipShape(Capsule()) .shadow(radius: 8) .coordinateSpace(name: "SegmentContainer") .onPreferenceChange(SegmentPreferenceKey.self) { segmentFrames = $0 } } // 用于标记是否处于动画状态 @State private var isAnimating = false } enum RotationCase: String, CaseIterable { case P1 = "P1", P2 = "P2", P3 = "P3", P4 = "P4", P5 = "P5", P6 = "P6" } // 预览 struct SegmentedControl_Previews: PreviewProvider { static var previews: some View { SegmentedControl() } }
关键实现点说明:
- 独立指示器:把白色背景做成单独的
RoundedRectangle,放在ZStack底层,文字层在上方,这样就能实现指示器的移动效果。 - 位置收集:用
GeometryReader和自定义PreferenceKey收集每个选项在容器内的位置和尺寸,确保指示器能精准移动到目标位置。 - 拉伸+移动组合动画:
- 点击时先触发横向拉伸动画(
scaleEffect),重复一次自动反转。 - 延迟一小段时间后,启动指示器的位置偏移动画。
- 动画结束后关闭拉伸状态,恢复圆形(这里用圆角矩形,和容器Capsule匹配,视觉上是圆形)。
- 点击时先触发横向拉伸动画(
- 动画时序控制:用
DispatchQueue延迟执行,让拉伸和移动动画衔接更自然,避免冲突。
优化建议:
- 可以调整动画的
duration和delay参数,找到最适合的流畅度。 - 如果需要更细腻的拉伸效果,可以根据移动距离动态调整拉伸比例(比如移动越远,拉伸幅度越大)。
内容的提问来源于stack exchange,提问作者carbonedavidee
相关产品推荐
相关产品推荐

