SwiftUI 仿小红书分页指示器, 支持轮播图组件

小红书使用红色圆点代表选中状态, 这里使用的是胶囊, 思路异曲同工, 直接上代码

import SwiftUI

struct AnimatedPageControl: View {
    @Binding var currentPage: Int
    @State var visibleRange = 0...4
    var totalPage: Int
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView(.horizontal, showsIndicators: false) {
                AnimatedPageWindow(
                    currentPage: $currentPage,
                    totalCount: totalPage,
                    visibleRange: visibleRange
                )
            }.frame(width: 56, height: 5)
            .onChange(of: visibleRange) { oldValue, newValue in
                if totalPage <= 5 {
                    return
                }
                if newValue.first == 0 {
                    DispatchQueue.main.async {
                        withAnimation {
                            proxy.scrollTo(newValue.first, anchor: .leading)
                        }
                    }
                    return
                }
                if newValue.last == totalPage - 1 {
                    DispatchQueue.main.async {
                        withAnimation {
                            proxy.scrollTo(newValue.first, anchor: .leading)
                        }
                    }
                    return
                }
                if newValue.last ?? 0 > oldValue.last ?? 0 {
                    DispatchQueue.main.async {
                        withAnimation {
                            proxy.scrollTo(newValue.last, anchor: .trailing)
                        }
                    }
                }
                
                if newValue.last ?? 0 < oldValue.last ?? 0 {
                    DispatchQueue.main.async {
                        withAnimation {
                            proxy.scrollTo(newValue.first, anchor: .leading)
                        }
                    }
                }
                
            }
        }
        .onChange(of: currentPage) { oldValue, newValue in
            if totalPage <= 5 {
                return
            }
            // 因为是轮播图, 考虑从 0 左滑, 从 totalPage - 1 右滑
            if oldValue == 0, newValue == totalPage - 1 {
                // 左滑, 轮播, 滑动到最后一个 traling
                
                //滑动后记得更新visibleRange
                visibleRange = totalPage - 5...totalPage - 1
            } else if oldValue == totalPage - 1, newValue == 0 {
                // 右滑, 轮播, 滑动到第一个, leading
                visibleRange = 0...4
                //滑动后记得更新visibleRange
            } else if newValue > oldValue {
                // 向右滑动, 如果右边还有, 向右滑动一格, 否则正常赋值胶囊
                if visibleRange.last == newValue && newValue < totalPage - 1 {
                    // 如果右边还有, 向右滑动一格
                    
                    // 滑动后记得更新visibleRange
                    let start = (visibleRange.first ?? 0) + 1
                    let last = (visibleRange.last ?? 0) + 1
                    visibleRange = start...last
                }
            } else {
                // 向左滑动, 如果左边还有
                if visibleRange.first == newValue && newValue != 0 {
                    // 如果左边还有, 向左滑动一格
                    
                    // 滑动后记得更新visibleRange
                    let start = (visibleRange.first ?? 0) - 1
                    let last = (visibleRange.last ?? 0) - 1
                    visibleRange = start...last
                }
            }
        }
    }
}



struct AnimatedPageWindow: View {
    @Binding var currentPage: Int
    let totalCount: Int
    let visibleRange: ClosedRange<Int>
    
    // MARK: 默认参数
    private let dotHeight: CGFloat = 5
    private let dotWidth: CGFloat = 5
    private let smalldotHeight: CGFloat = 3
    private let smalldotWidth: CGFloat = 3
    private let selectedExtraWidth: CGFloat = 7
    private let spacing: CGFloat = 6
    
    var body: some View {
        HStack(spacing: spacing) {
            ForEach(0..<totalCount, id: \.self) { index in
                Capsule()
                    .fill(index == currentPage
                          ? Color.black.opacity(0.45)
                          : Color.black.opacity(0.15))
                    .frame(
                        width: index == currentPage
                            ? dotWidth + selectedExtraWidth
                        : isSmallDot(index) ? smalldotWidth : dotWidth,
                        height: isSmallDot(index) ? smalldotHeight : dotHeight
                    )
                    // 左右小点:本质 5x5,通过 padding 压成 3x3
                    .padding(.all, isSmallDot(index) ? 1 : 0)
                    .animation(.spring(response: 0.25, dampingFraction: 0.55), value: currentPage)
                    .id(index)
                
            }
        }
    }
    
    private func isSmallDot(_ index: Int) -> Bool {
        // 当前页永远不是小点
        if index == currentPage {
            return false
        }
        
        // ❗️不可见范围:永远是小点
        if !visibleRange.contains(index) {
            return true
        }
        
        // 左边界小点(左侧还有页)
        if index == visibleRange.first && visibleRange.first ?? 0 > 0 {
            return true
        }
        
        // 右边界小点(右侧还有页)
        if index == visibleRange.last && visibleRange.last ?? 0 < totalCount - 1 {
            return true
        }
        
        return false
    }
}
 

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容