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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 10:14:51