SwiftUI中如何实现滑块联动的平滑星形旋转滑动动画?
SwiftUI Slider 星形动画复现问题
我尝试用SwiftUI默认Slider复现如下滑块动画:
原动画里星形会伴随旋转与滑动动作,我已经实现了部分效果,但动画不如原示例平滑,而且滑块接近数值范围末端时星形定位有偏移。怎么实现和目标动画一致的效果?
我的代码如下:
struct ContentView: View { @State private var value = 0.0 @State private var previousValue = 0.0 @State private var rotationAngle = 0.0 private var starSize = 30.0 var body: some View { VStack(spacing: 90) { Text("Rating: \(Int(value))/10") GeometryReader { proxy in VStack { Spacer() Slider(value: $value, in: 0...10, step: 1) .overlay(alignment: .leading) { Image(systemName: "star.fill") .resizable() .scaledToFit() .frame(width: starSize, height: starSize) .foregroundStyle(.gray) .rotationEffect(.degrees(rotationAngle)) .offset(x: xOffset(proxy: proxy), y: yOffset()) .animation(.spring(duration: 0.8, bounce: value == 0 ? 0 : 0.2).delay(0.1), value: value) .onChange(of: value) { oldValue, newValue in if newValue > oldValue { rotationAngle = -20 } else if newValue < oldValue { rotationAngle = 20 } withAnimation(.spring(duration: 0.4).delay(0.2)) { rotationAngle = 0 } previousValue = newValue } .allowsHitTesting(false) } .background { Color.yellow } } .background { Color.green } } .frame(height: 80) .padding(.horizontal) } } private func xOffset(proxy: GeometryProxy) -> CGFloat { guard value > 0 else { return 0 } let sliderWidth = proxy.size.width let position = sliderWidth * (value / 10) return position - (starSize / 2) } private func yOffset() -> CGFloat { guard value > 0 else { return 0 } return -1.5 * starSize } }
问题解决方案
1. 修复星形末端定位偏移问题
你的xOffset计算没有考虑Slider默认的左右内边距,导致用整个容器宽度计算的滑动位置比实际可滑动区域宽,末端时星形会出现偏移。
修改xOffset方法,去除Slider内置内边距后计算实际可滑动范围:
private func xOffset(proxy: GeometryProxy) -> CGFloat { guard value > 0 else { return 0 } // SwiftUI Slider默认有16pt左右内边距,适配大部分系统版本 let sliderInsets = EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) // 计算实际可滑动的宽度 let usableWidth = proxy.size.width - sliderInsets.leading - sliderInsets.trailing // 从Slider左内边距开始计算星形位置 let position = sliderInsets.leading + usableWidth * (value / 10) return position - (starSize / 2) }
2. 优化动画平滑度
当前动画拆分了多个独立动画块,导致衔接生硬。可以合并旋转、位移动画的节奏,让动画同步触发:
- 统一动画曲线,使用更贴合目标效果的弹簧参数
- 让旋转复位动画和位移动画的节奏匹配
- 增加微小的上下弹跳效果,还原目标动画细节
完整修改后的代码
struct ContentView: View { @State private var value = 0.0 @State private var rotationAngle = 0.0 @State private var bounceOffset = 0.0 private var starSize = 30.0 var body: some View { VStack(spacing: 90) { Text("Rating: \(Int(value))/10") GeometryReader { proxy in VStack { Spacer() Slider(value: $value, in: 0...10, step: 1) .overlay(alignment: .leading) { Image(systemName: "star.fill") .resizable() .scaledToFit() .frame(width: starSize, height: starSize) .foregroundStyle(.gray) .rotationEffect(.degrees(rotationAngle)) .offset(x: xOffset(proxy: proxy), y: yOffset()) // 统一使用弹簧动画,让滑动和旋转同步 .animation(.interpolatingSpring(stiffness: 300, damping: 30, initialVelocity: 10), value: value) .onChange(of: value) { oldValue, newValue in // 根据滑动方向设置旋转角度 let rotationDelta = newValue > oldValue ? -20.0 : 20.0 rotationAngle = rotationDelta // 添加微小向下弹跳效果 bounceOffset = -5 // 延迟复位旋转和弹跳,匹配滑动动画节奏 DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { withAnimation(.interpolatingSpring(stiffness: 300, damping: 30)) { rotationAngle = 0 bounceOffset = 0 } } } .allowsHitTesting(false) } .background { Color.yellow } } .background { Color.green } } .frame(height: 80) .padding(.horizontal) } } private func xOffset(proxy: GeometryProxy) -> CGFloat { guard value > 0 else { return 0 } let sliderInsets = EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) let usableWidth = proxy.size.width - sliderInsets.leading - sliderInsets.trailing let position = sliderInsets.leading + usableWidth * (value / 10) return position - (starSize / 2) } private func yOffset() -> CGFloat { guard value > 0 else { return 0 } // 加入弹跳偏移量 return -1.5 * starSize + bounceOffset } }
内容的提问来源于stack exchange,提问作者batman
相关产品推荐
相关产品推荐

