如何在SwiftUI中控制动画进度(类似UIKit的fractionComplete)
SwiftUI 手动控制动画进度(类似UIKit的UIViewPropertyAnimator.fractionComplete)
问题描述
在SwiftUI中实现视图在两种状态间的动画时,如何摆脱仅在状态间切换的限制,实现类似UIKit中UIViewPropertyAnimator的fractionComplete功能,手动设置特定动画进度?比如跟随滑块值、Socket数据或滚动视图偏移量(0-1的CGFloat)来静态呈现对应进度的状态,而非自动播放完整动画。
现有点击切换状态的参考代码:
struct TransitionView: View { @Namespace var namespace @State var isExpanded: Bool = false @ViewBuilder var body: some View { VStack { switch isExpanded { case true: VStack { Circle() .strokeBorder(Color.green, lineWidth: 5) .matchedGeometryEffect(id: "circle", in: namespace) .frame(width: 70, height: 70) Text(verbatim: "I am Circle") .matchedGeometryEffect(id: "text", in: namespace) } case false: HStack { Circle() .strokeBorder(Color.red, lineWidth: 5) .matchedGeometryEffect(id: "circle", in: namespace) .frame(width: 30, height: 30) Text(verbatim: "I am Circle") .matchedGeometryEffect(id: "text", in: namespace) } } } .contentShape(Rectangle()) .animation(.easeInOut, value: isExpanded) .onTapGesture { isExpanded.toggle() } } }

解决方案
核心思路是用0-1范围的进度变量替代布尔状态,手动计算所有过渡属性的插值,从而实现任意进度的静态状态呈现。
修改后的完整代码
struct TransitionView: View { @Namespace var namespace @State var progress: CGFloat = 0 // 0 = 未展开状态,1 = 完全展开状态 // 计算圆的大小插值:从30到70平滑过渡 private var circleSize: CGFloat { 30 + (70 - 30) * progress } // 计算颜色插值:从红色到绿色平滑过渡 private var circleColor: Color { Color.red.interpolated(to: .green, fraction: progress) } var body: some View { VStack(spacing: 20) { // 用ZStack实现布局的平滑过渡,避免切换容器的生硬感 ZStack(alignment: .leading) { Circle() .strokeBorder(circleColor, lineWidth: 5) .frame(width: circleSize, height: circleSize) .matchedGeometryEffect(id: "circle", in: namespace) Text("I am Circle") .matchedGeometryEffect(id: "text", in: namespace) // 根据进度计算垂直偏移,实现从右侧到下方的布局过渡 .offset(x: (1 - progress) * 40, y: progress * 80) } .frame(height: 100) // 预留足够空间容纳两种布局 // 测试用滑块:实际场景可替换为滚动偏移、Socket数据等数据源 Slider(value: $progress, in: 0...1) .padding(.horizontal, 20) } .animation(.easeInOut, value: progress) .contentShape(Rectangle()) } } // 扩展Color实现RGB颜色插值 extension Color { func interpolated(to other: Color, fraction: CGFloat) -> Color { let clampedFraction = fraction.clamped(to: 0...1) let selfComponents = self.components let otherComponents = other.components return Color( red: selfComponents.red + (otherComponents.red - selfComponents.red) * clampedFraction, green: selfComponents.green + (otherComponents.green - selfComponents.green) * clampedFraction, blue: selfComponents.blue + (otherComponents.blue - selfComponents.blue) * clampedFraction, opacity: selfComponents.opacity + (otherComponents.opacity - selfComponents.opacity) * clampedFraction ) } // 提取Color的RGB和透明度组件 private var components: (red: CGFloat, green: CGFloat, blue: CGFloat, opacity: CGFloat) { let uiColor = UIColor(self) var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) return (red, green, blue, alpha) } }
关键实现细节
- 进度变量替换布尔状态:用
progress: CGFloat(0-1范围)代替isExpanded: Bool,直接映射动画的中间进度 - 属性插值计算:对需要过渡的属性(大小、颜色、位置)手动计算插值,确保任意进度下都有对应的静态状态
- 平滑布局过渡:用
ZStack结合offset实现布局的连续过渡,避免切换VStack/HStack带来的生硬感 - 灵活数据源适配:测试用的滑块可直接替换为滚动视图偏移量、Socket推送的进度值等外部数据源
内容的提问来源于stack exchange,提问作者bodich
相关产品推荐
相关产品推荐

