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

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)
    }
}

逻辑说明

  1. 把计算圆形中心的逻辑抽成公共方法,避免重复代码;
  2. 用GeometryReader获取当前视图的尺寸,计算出圆形的实时中心位置;
  3. 将图片设置为固定大小后裁剪成圆形,通过position(center)绑定到计算出的中心,这样图片会跟随中心的波浪运动同步位移;
  4. 保留原有的波浪动画逻辑,不影响整体效果。

内容的提问来源于stack exchange,提问作者dub

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 05:22:02