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

如何修复SwiftUI横向无限滚动按钮的偏移重置与动画卡顿

横向滚动按钮的无缝循环与卡顿修复方案

问题根源

  1. 末尾跳变:直接将offsetX重置为0无过渡动画,且总宽度计算逻辑错误(单个按钮宽度×行数≠每行实际总宽度),导致重置时出现视觉断层。
  2. 动画卡顿:Timer配合频繁withAnimation触发重复布局,加上每个按钮嵌套GeometryReader,增加了不必要的布局计算开销。

修复实现

核心优化思路

  • 无缝循环:给每行的按钮列表复制一份并放在原列表末尾,当滚动完第一份内容时,第二份内容的开头刚好衔接第一份结尾,此时重置偏移量用户完全感知不到。
  • 流畅动画:用SwiftUI原生的无限重复动画替代Timer,减少布局触发频率;同时优化宽度测量逻辑,仅在整行层面测量一次宽度。

完整修复代码

import SwiftUI

struct AutoScrollingButtonsView: View {
    let buttonData = [
        ["🕯️ Did Edison invent the lightbulb?", "🚴‍♂️ How do you ride a bike?"],
        ["⛷️ The best ski resorts in the Alps", "🏟️ The greatest Super Bowl moments", "🎬 Best movies of all time"],
        ["🥊 The best boxing style", "🐩 The best dog breed for apartments","🏖️ Top beach destinations"],
    ]
    
    @State private var rowTotalWidths: [CGFloat] = Array(repeating: 0, count: 3)
    @State private var isScrolling = false
    
    var body: some View {
        VStack(spacing: 20) {
            ForEach(0..<buttonData.count, id: \.self) { rowIndex in
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: 16) {
                        // 原内容 + 复制一份,实现无缝衔接
                        ForEach(buttonData[rowIndex], id: \.self) { item in
                            buttonView(for: item)
                        }
                        ForEach(buttonData[rowIndex], id: \.self) { item in
                            buttonView(for: item)
                        }
                    }
                    .background(
                        GeometryReader { geo in
                            Color.clear.onAppear {
                                // 记录单份内容的宽度(因为复制了两份,取总宽度的一半)
                                rowTotalWidths[rowIndex] = geo.size.width / 2
                            }
                        }
                    )
                    .offset(x: isScrolling ? -rowTotalWidths[rowIndex] : 0)
                    .onAppear {
                        guard !isScrolling else { return }
                        startAutoScroll(for: rowIndex)
                    }
                }
            }
        }
        .frame(height: 200)
        .frame(maxWidth: .infinity)
    }
    
    // 复用按钮视图,减少重复代码
    private func buttonView(for text: String) -> some View {
        Button(action: {
            // 按钮点击逻辑
        }) {
            Text(text)
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(5)
                .foregroundColor(.black)
        }
    }
    
    private func startAutoScroll(for rowIndex: Int) {
        // 计算滚动时长:行宽度 / 滚动速度(50点/秒,可根据需求调整)
        let scrollDuration = rowTotalWidths[rowIndex] / 50
        
        withAnimation(.linear(duration: scrollDuration).repeatForever(autoreverses: false)) {
            isScrolling = true
        }
    }
}

struct AutoScrollingButtonsView_Previews: PreviewProvider {
    static var previews: some View {
        AutoScrollingButtonsView()
    }
}

关键细节说明

  • 无缝循环逻辑:每行HStack包含两份相同的按钮列表,当动画将偏移量设为-rowTotalWidths[rowIndex]时,刚好滚动完第一份内容,此时第二份的开头和第一份结尾完全一致,视觉上实现无缝衔接;动画无限重复,持续滚动。
  • 流畅性优化:用repeatForever动画替代Timer,避免了定时器频繁触发布局更新;将宽度测量移到整行的背景中,每个行只测量一次,减少布局计算量。
  • 速度一致性:通过行宽度除以固定速度计算滚动时长,确保不同长度的行滚动速度统一。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 01:27:04