SwiftUI中如何让图形填充的图片随图形移动而非固定
解决圆形头像随波浪同步移动的问题
问题根源是ImagePaint基于全局坐标系平铺图片,Shape移动时填充图不会同步位移。换个思路:别用Shape填充图片,直接把图片作为独立视图,跟着圆形位置移动后再裁剪。
修改后的核心代码
单圆形动画视图
struct WaveAnimationView: View { @State private var screenCoveragePercent = 45.0 @State private var waveOffset = Angle(degrees: 0) // 抽取计算圆形中心的公共逻辑 private func circleCenter(in rect: CGRect) -> CGPoint { let lowestWave = 0.02 let highestWave = 1.00 let newPercent = lowestWave + (highestWave - lowestWave) * (screenCoveragePercent / 100) let waveHeight = 0.0020 * rect.height let yOffSet = CGFloat(1 - newPercent) * (rect.height - 4 * waveHeight) + 2 * waveHeight let startAngle = waveOffset let endAngle = waveOffset + Angle(degrees: 360 + 10) var circleCenter = CGPoint.zero for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) { let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width let y = yOffSet + waveHeight * CGFloat(sin(Angle(degrees: angle).radians)) if x == rect.midX { circleCenter = CGPoint(x: x, y: y) } } if circleCenter == .zero { circleCenter = CGPoint(x: rect.midX, y: yOffSet + waveHeight * CGFloat(sin(Angle(degrees: waveOffset.degrees + 180).radians))) } return circleCenter } var body: some View { GeometryReader { geo in let center = circleCenter(in: geo.frame(in: .local)) let circleRadius: CGFloat = 50 Image("ryan") .resizable() .scaledToFill() // 保持图片比例,超出部分裁剪 .frame(width: 2 * circleRadius, height: 2 * circleRadius) .clipShape(Circle()) // 裁剪成圆形 .position(center) // 定位到计算出的中心位置 } .ignoresSafeArea(.all) .onAppear { withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) { self.waveOffset = Angle(degrees: 360) } } } }
带波浪背景的完整动画视图
struct WaveAnimation: View { @State private var screenCoveragePercent = 25.0 @State private var waveOffset = Angle(degrees: 0) // 复用圆形中心计算逻辑 private func circleCenter(in rect: CGRect) -> CGPoint { let lowestWave = 0.02 let highestWave = 1.00 let newPercent = lowestWave + (highestWave - lowestWave) * (screenCoveragePercent / 100) let waveHeight = 0.0020 * rect.height let yOffSet = CGFloat(1 - newPercent) * (rect.height - 4 * waveHeight) + 2 * waveHeight let startAngle = waveOffset let endAngle = waveOffset + Angle(degrees: 360 + 10) var circleCenter = CGPoint.zero for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) { let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width let y = yOffSet + waveHeight * CGFloat(sin(Angle(degrees: angle).radians)) if x == rect.midX { circleCenter = CGPoint(x: x, y: y) } } if circleCenter == .zero { circleCenter = CGPoint(x: rect.midX, y: yOffSet + waveHeight * CGFloat(sin(Angle(degrees: waveOffset.degrees + 180).radians))) } return circleCenter } var body: some View { ZStack { let opacityOfWater = 0.25 // 波浪背景 Wave(offSet: Angle(degrees: waveOffset.degrees), percent: screenCoveragePercent) .fill(Color.blue).opacity(opacityOfWater) .ignoresSafeArea(.all) // 可移动的圆形头像 GeometryReader { geo in let center = circleCenter(in: geo.frame(in: .local)) let circleRadius: CGFloat = 50 Image("ryan") .resizable() .scaledToFill() .frame(width: 2 * circleRadius, height: 2 * circleRadius) .clipShape(Circle()) .position(center) } InvisibleSlider(percent: $screenCoveragePercent) } .onAppear { withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) { self.waveOffset = Angle(degrees: 360) } } } } // 保留原Wave结构不变 struct Wave: Shape { var offSet: Angle var percent: Double var animatableData: Double { get { offSet.degrees } set { offSet = Angle(degrees: newValue) } } func path(in rect: CGRect) -> Path { var p = Path() let lowestWave = 0.02 let highestWave = 1.00 let newPercent = lowestWave + (highestWave - lowestWave) * (percent / 100) let waveHeight = 0.0020 * rect.height let yOffSet = CGFloat(1 - newPercent) * (rect.height - 4 * waveHeight) + 2 * waveHeight let startAngle = offSet let endAngle = offSet + Angle(degrees: 360 + 10) p.move(to: CGPoint(x: 0, y: yOffSet + waveHeight * CGFloat(sin(offSet.radians)))) for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) { let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width let y = yOffSet + waveHeight * CGFloat(sin(Angle(degrees: angle).radians)) p.addLine(to: CGPoint(x: x, y: y)) } p.addLine(to: CGPoint(x: rect.width, y: rect.height)) p.addLine(to: CGPoint(x: 0, y: rect.height)) p.closeSubpath() return p } } // 假设InvisibleSlider是你定义的滑块视图 struct InvisibleSlider: View { @Binding var percent: Double var body: some View { Slider(value: $percent, in: 0...100) .opacity(0) } }
逻辑说明
- 把计算圆形中心的逻辑抽成公共方法,避免重复代码;
- 用
GeometryReader获取当前视图的尺寸,计算出圆形的实时中心位置; - 将图片设置为固定大小后裁剪成圆形,通过
position(center)绑定到计算出的中心,这样图片会跟随中心的波浪运动同步位移; - 保留原有的波浪动画逻辑,不影响整体效果。
内容的提问来源于stack exchange,提问作者dub
相关产品推荐
相关产品推荐

