SwiftUI ScrollView停止滚动后选中值居中动画实现求助
问题解决:HorizontalPicker滚动停止后自动居中选中值
核心问题分析
当前代码通过监听每个item的位置变化来切换选中值,这种方式会在滚动过程中频繁触发选中状态变更,且滚动停止后没有强制将最终选中的值对齐到屏幕中心,导致出现选中值偏离中心的情况。
解决方案
我们需要在滚动停止后再触发选中值的判定与居中操作,而不是滚动过程中实时监听。以下是适配iOS 17+的优化实现(如果需要兼容更低版本,可通过UIViewRepresentable包装UIScrollView获取滚动停止事件):
import SwiftUI struct HorizontalPicker<T: Hashable & CustomStringConvertible>: View { @Binding var selectedValue: T let values: [T] let itemWidth: CGFloat = 80 // 存储每个item的全局位置 @State private var itemFrames: [T: CGRect] = [:] var body: some View { VStack(alignment: .leading, spacing: 30) { VStack { Text("Select Value") .fontWeight(.semibold) ZStack { Circle() .frame(width: 60) .foregroundColor(.green) ScrollView(.horizontal, showsIndicators: false) { ScrollViewReader { scrollViewProxy in HStack(spacing: 20) { Spacer() .frame(width: (UIScreen.main.bounds.width / 2) - (itemWidth / 2)) ForEach(values, id: \.self) { value in Text("\(value.description)") .font(.title2) .fontWeight(.bold) .padding() .foregroundColor(selectedValue == value ? .white : .black) .background( Circle() .frame(width: 40, height: 40) .foregroundColor(selectedValue == value ? .blue : .clear) ) .background(GeometryReader { geo in Color.clear .onAppear { // 记录item的初始位置 itemFrames[value] = geo.frame(in: .global) } .onChange(of: geo.frame(in: .global)) { newFrame in // 更新item滚动时的位置 itemFrames[value] = newFrame } }) .onTapGesture { withAnimation { selectedValue = value scrollViewProxy.scrollTo(value, anchor: .center) } } .id(value) } Spacer() .frame(width: (UIScreen.main.bounds.width / 2) - (itemWidth / 2)) } .onAppear { DispatchQueue.main.async { scrollViewProxy.scrollTo(selectedValue, anchor: .center) } } } } .frame(height: 100) // iOS 17+ 滚动停止回调 .scrollEnded { direction in guard let closestValue = findClosestValueToCenter() else { return } withAnimation { selectedValue = closestValue // 强制滚动到中心 ScrollViewReader { proxy in proxy.scrollTo(closestValue, anchor: .center) } } } } } Spacer() VStack(alignment: .leading) { Text("Selected Value: \(selectedValue.description)") } } .padding() } // 找到最靠近屏幕中心的item private func findClosestValueToCenter() -> T? { let screenCenterX = UIScreen.main.bounds.width / 2 var closestValue: T? var minDistance: CGFloat = .infinity for (value, frame) in itemFrames { let itemCenterX = frame.midX let distance = abs(itemCenterX - screenCenterX) if distance < minDistance { minDistance = distance closestValue = value } } return closestValue } } // Preview struct HorizontalPickerPreview: View { @State private var selectedInt = 110 @State private var selectedString = "Apple" @State private var selectedFloat: Float = 5.5 var body: some View { VStack(spacing: 30) { HorizontalPicker(selectedValue: $selectedInt, values: Array(80...200)) .frame(height: 150) HorizontalPicker(selectedValue: $selectedString, values: ["Apple", "Banana", "Cherry", "Date", "Elderberry"]) .frame(height: 150) HorizontalPicker(selectedValue: $selectedFloat, values: stride(from: 1.0, through: 10.0, by: 0.5).map { Float($0) }) .frame(height: 150) } } } #Preview { HorizontalPickerPreview() }
关键改动说明
- 滚动停止触发逻辑:使用iOS 17新增的
scrollEnded修饰符,仅在滚动完全停止后执行选中值判定与居中操作,避免滚动过程中的频繁切换。 - 位置记录与计算:通过
GeometryReader记录每个item的全局位置,滚动停止时遍历所有item,计算出最靠近屏幕中心的那个值。 - 强制居中对齐:选中目标值后,通过
ScrollViewReader强制将其滚动到屏幕中心,确保最终显示位置符合预期。 - 移除实时监听:删除原有的
onChange(of: midX)逻辑,避免滚动过程中误触发选中状态。
如果需要兼容iOS 16及以下版本,可以通过UIViewRepresentable包装UIScrollView,利用UIScrollViewDelegate的scrollViewDidEndDecelerating和scrollViewDidEndDragging方法来监听滚动停止事件,核心逻辑与上述实现一致。
内容的提问来源于stack exchange,提问作者Abdul Rehman
相关产品推荐
相关产品推荐

