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

SwiftUI中如何实现图片的间歇性无限循环横向移动?

实现SwiftUI横向图片的间歇性无缝滚动动画

针对你要的「移动100pt后暂停、再继续向左滚动」的需求,可以放弃原来的alignment动画,改用offset精确控制移动距离,结合分步动画+延迟暂停的方式实现。以下是修改后的代码:

基础版本(已知图片宽度)

struct AnimatingHorizontalImageView: View {
    // 控制图片横向偏移量
    @State private var offsetX: CGFloat = 0
    // 单次移动距离(设置为你需要的100pt)
    private let stepOffset: CGFloat = -100
    // 单次移动的时长(可根据需求调整快慢)
    private let moveDuration: Double = 1
    // 每次移动后的暂停时长
    private let pauseDuration: Double = 2
    // 屏幕宽度
    private let screenWidth = UIScreen.main.bounds.width
    // 图片原始宽度(根据你的实际图片尺寸修改)
    private let imageOriginalWidth: CGFloat = 600
    
    // 计算滚动一轮需要的总步数
    private var totalSteps: Int {
        Int(ceil((imageOriginalWidth - screenWidth) / abs(stepOffset)))
    }
    
    var body: some View {
        // 两张图片无缝拼接实现滚动
        HStack(spacing: 0) {
            Image(.banner)
                .resizable()
                .scaledToFill()
                .frame(height: 210)
            
            Image(.banner)
                .resizable()
                .scaledToFill()
                .frame(height: 210)
        }
        // 限制显示区域为屏幕宽度,超出部分裁剪
        .frame(width: screenWidth, height: 210)
        .clipped()
        // 应用偏移量动画
        .offset(x: offsetX)
        .onAppear {
            startIntermittentAnimation()
        }
    }
    
    // 递归执行间歇性动画逻辑
    private func startIntermittentAnimation() {
        func animateStep(currentStep: Int) {
            // 执行单次100pt的移动动画
            withAnimation(.linear(duration: moveDuration)) {
                offsetX += stepOffset
            }
            
            // 等待动画完成+暂停时长后,执行下一步
            DispatchQueue.main.asyncAfter(deadline: .now() + moveDuration + pauseDuration) {
                if currentStep < totalSteps {
                    // 还没滚完一轮,继续下一次移动
                    animateStep(currentStep: currentStep + 1)
                } else {
                    // 滚完一轮后重置偏移量,等待暂停后重新开始
                    withAnimation(.linear(duration: 0)) {
                        offsetX = 0
                    }
                    DispatchQueue.main.asyncAfter(deadline: .now() + pauseDuration) {
                        animateStep(currentStep: 1)
                    }
                }
            }
        }
        
        animateStep(currentStep: 1)
    }
}

进阶版本(自动获取图片宽度)

如果不确定图片的实际宽度,可以用GeometryReader动态获取,避免硬编码:

struct AnimatingHorizontalImageView: View {
    @State private var offsetX: CGFloat = 0
    @State private var imageOriginalWidth: CGFloat = 0
    private let stepOffset: CGFloat = -100
    private let moveDuration: Double = 1
    private let pauseDuration: Double = 2
    private let screenWidth = UIScreen.main.bounds.width
    
    private var totalSteps: Int {
        guard imageOriginalWidth > screenWidth else { return 0 }
        return Int(ceil((imageOriginalWidth - screenWidth) / abs(stepOffset)))
    }
    
    var body: some View {
        HStack(spacing: 0) {
            Image(.banner)
                .resizable()
                .scaledToFill()
                .frame(height: 210)
                // 通过GeometryReader获取图片实际宽度
                .background(
                    GeometryReader { proxy in
                        Color.clear.onAppear {
                            imageOriginalWidth = proxy.size.width
                        }
                    }
                )
            
            Image(.banner)
                .resizable()
                .scaledToFill()
                .frame(height: 210)
        }
        .frame(width: screenWidth, height: 210)
        .clipped()
        .offset(x: offsetX)
        .onAppear {
            // 延迟确保获取到图片宽度后再启动动画
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                startIntermittentAnimation()
            }
        }
    }
    
    private func startIntermittentAnimation() {
        guard totalSteps > 0 else { return }
        
        func animateStep(currentStep: Int) {
            withAnimation(.linear(duration: moveDuration)) {
                offsetX += stepOffset
            }
            
            DispatchQueue.main.asyncAfter(deadline: .now() + moveDuration + pauseDuration) {
                if currentStep < totalSteps {
                    animateStep(currentStep: currentStep + 1)
                } else {
                    withAnimation(.linear(duration: 0)) {
                        offsetX = 0
                    }
                    DispatchQueue.main.asyncAfter(deadline: .now() + pauseDuration) {
                        animateStep(currentStep: 1)
                    }
                }
            }
        }
        
        animateStep(currentStep: 1)
    }
}

关键说明

  • 用offset替代alignment:offset能精确控制每次移动的像素距离,比alignment更适合分步移动的需求。
  • 分步递归动画:通过递归调用实现「移动→暂停→移动」的循环逻辑,每一步都严格控制移动距离和暂停时长。
  • 无缝重置:当图片滚动完一整轮后,立即重置偏移量为0,保证滚动的无缝衔接。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 17:57:11