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
相关产品推荐
相关产品推荐

