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

如何在SwiftUI中为图片轮播的页码指示器实现鱼眼效果?

SwiftUI 图片轮播鱼眼页码指示器实现方案

核心实现思路

要实现鱼眼效果的页码指示器,关键是根据当前轮播的激活位置,动态计算每个指示器的缩放比例:

  1. 绑定轮播的当前位置(可以是整数索引,也可以是滚动偏移的浮点进度)
  2. 计算每个指示器与当前位置的距离差
  3. 通过非线性缩放公式,让距离当前位置越近的指示器缩放越大,越远的越小,形成鱼眼视觉效果

代码实现示例

方案1:基于TabView的离散索引鱼眼效果

适合分页式轮播,切换页面时指示器缩放动画过渡:

struct FishEyePageIndicator: View {
    // 模拟轮播图片数据
    private let carouselItems = ["img1", "img2", "img3", "img4", "img5"]
    // 当前选中的页码索引
    @State private var currentPage = 0
    
    // 鱼眼效果参数,可按需调整
    private let baseIndicatorSize: CGFloat = 10
    private let maxScale: CGFloat = 1.5
    private let minScale: CGFloat = 0.7
    private let scaleDecayFactor: CGFloat = 0.6
    
    var body: some View {
        VStack {
            // 图片轮播容器
            TabView(selection: $currentPage) {
                ForEach(carouselItems.indices, id: \.self) { index in
                    Image(carouselItems[index])
                        .resizable()
                        .scaledToFill()
                        .frame(height: 250)
                        .clipped()
                        .tag(index)
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
            .animation(.easeInOut, value: currentPage)
            
            // 自定义鱼眼指示器
            HStack(spacing: 8) {
                ForEach(carouselItems.indices, id: \.self) { index in
                    Circle()
                        .fill(index == currentPage ? .blue : .gray.opacity(0.5))
                        .frame(width: baseIndicatorSize, height: baseIndicatorSize)
                        .scaleEffect(scaleForIndicator(at: index))
                        .animation(.easeInOut(duration: 0.3), value: currentPage)
                }
            }
            .padding(.top, 16)
        }
    }
    
    // 计算单个指示器的缩放比例
    private func scaleForIndicator(at index: Int) -> CGFloat {
        let distance = abs(currentPage - index)
        // 非线性衰减公式:距离越近,缩放越大;距离超过2后固定最小缩放
        if distance >= 2 {
            return minScale
        }
        return maxScale - (maxScale - minScale) * pow(scaleDecayFactor, CGFloat(distance))
    }
}

方案2:基于ScrollView的平滑实时鱼眼效果

支持拖拽过程中指示器实时缩放,过渡更流畅:

struct SmoothFishEyeIndicator: View {
    private let carouselItems = ["img1", "img2", "img3", "img4", "img5"]
    // 滚动偏移量,用于计算当前轮播进度
    @State private var scrollOffset: CGFloat = 0
    private let itemWidth = UIScreen.main.bounds.width
    
    // 鱼眼效果参数
    private let baseIndicatorSize: CGFloat = 10
    private let maxScale: CGFloat = 1.5
    private let minScale: CGFloat = 0.7
    
    var body: some View {
        VStack {
            // 横向滚动轮播
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 0) {
                    ForEach(carouselItems.indices, id: \.self) { index in
                        Image(carouselItems[index])
                            .resizable()
                            .scaledToFill()
                            .frame(width: itemWidth, height: 250)
                            .clipped()
                    }
                }
                .background(
                    GeometryReader { proxy in
                        Color.clear
                            .preference(key: ScrollOffsetKey.self, value: proxy.frame(in: .named("carouselScroll")).minX)
                    }
                )
            }
            .coordinateSpace(name: "carouselScroll")
            .onPreferenceChange(ScrollOffsetKey.self) { offset in
                scrollOffset = offset
            }
            
            // 实时缩放的鱼眼指示器
            HStack(spacing: 8) {
                ForEach(carouselItems.indices, id: \.self) { index in
                    Circle()
                        .fill(indicatorColor(for: index))
                        .frame(width: baseIndicatorSize, height: baseIndicatorSize)
                        .scaleEffect(scaleForIndicator(at: index))
                }
            }
            .padding(.top, 16)
        }
    }
    
    // 实时计算缩放比例
    private func scaleForIndicator(at index: Int) -> CGFloat {
        let currentProgress = -scrollOffset / itemWidth
        let distance = abs(currentProgress - CGFloat(index))
        
        // 用余弦曲线实现平滑过渡:距离0时maxScale,距离2时minScale
        if distance > 2 {
            return minScale
        }
        return maxScale - (maxScale - minScale) * (1 - cos(distance * .pi / 2)) / 2
    }
    
    // 指示器颜色随距离动态变化
    private func indicatorColor(for index: Int) -> Color {
        let currentProgress = -scrollOffset / itemWidth
        let distance = abs(currentProgress - CGFloat(index))
        return distance < 0.5 ? .blue : .gray.opacity(0.5 + (1 - distance) * 0.5)
    }
}

// 自定义PreferenceKey,用于获取滚动偏移量
private struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

优化建议

  • 调整缩放公式:可以根据设计需求修改缩放逻辑,比如用1 / (1 + 0.4 * distance)实现反比例衰减,或者二次函数让缩放变化更陡
  • 动画优化:替换.easeInOut为.spring(response: 0.3, dampingFraction: 0.7),让指示器缩放更有弹性
  • 响应式适配:将指示器的基础大小、间距改为相对单位(比如UIScreen.main.bounds.width * 0.025),适配不同屏幕
  • 性能优化:当指示器数量超过8个时,改用LazyHStack避免一次性渲染所有视图
  • 视觉增强:给选中的指示器添加阴影(.shadow(color: .blue.opacity(0.3), radius: 3)),提升层次感

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 06:00:04