SwiftUI 一种旋转木马效果的实现(Carousel)

Demo实现原理:
首先确定 5 个位置信息,移动只是把5个位置信息重新设置到5个方块,把赋值语句放到withAnimation { } 中,即可产生动画位移动画效果。其中麻烦的是zIndex。

确定位置信息

为了方便实现原理,这里使用硬编码的方式,实际项目,根据项目求算位置信息。

struct HomeCarousel: View {
    struct Slot{
        var id: String
        var scale: CGFloat
        var offsetX: CGFloat
    }
    
    struct Cell: Identifiable {
        var id: String
        var color: Color
    }
    
    let standarCellSize = CGSize(width: 109, height: 330)
    let Slots: [Slot] = [
        Slot(id: "left",scale: 0.6, offsetX: -196.2),
        Slot(id: "left1",scale: 0.8, offsetX: -109),
        Slot(id: "middle",scale: 1, offsetX: 0),
        Slot(id: "right1",scale: 0.8, offsetX: 109),
        Slot(id: "right",scale: 0.6, offsetX: 196.2)
    ]
    
    let myCells: [Cell] = [
        Cell(id: "hanfu0", color: .yellow),
        Cell(id: "hanfu1", color: .red),
        Cell(id: "hanfu2", color: .green),
        Cell(id: "hanfu3", color: .blue),
        Cell(id: "hanfu4", color: .gray),
    ]
    
    @State var cellSlotDic: Dictionary<String, Slot>
    @State var cellZIndexDic: Dictionary<String,Double>
    
    init() {
    _cellSlotDic = State(initialValue: [
        "hanfu0": Slots[0],
        "hanfu1": Slots[1],
        "hanfu2": Slots[2],
        "hanfu3": Slots[3],
        "hanfu4": Slots[4],
        ])
        
        _cellZIndexDic = State(initialValue: [
            "hanfu0": 2.0,
            "hanfu1": 2.0,
            "hanfu2": 2.0,
            "hanfu3": 2.0,
            "hanfu4": 2.0,
        ])
        
    }
    
}

上述代码用key: value的方式,把view和 Slot 、index对应起来。

显示view

根据cell.id从字典中取出渲染数值.

    var body: some View {
        ZStack {
            ForEach(myCells) { v in
                Rectangle()
                    .overlay(
                        Text(v.id)
                            .foregroundColor(.white)
                    )
                    .foregroundColor(v.color)
                    .frame(width: standarCellSize.width, height: standarCellSize.height)
                    .scaleEffect(cellSlotDic[v.id]!.scale)
                    .offset(x: cellSlotDic[v.id]!.offsetX)
                    .zIndex(cellZIndexDic[v.id]!)
            }
            
            HStack {
                Button("<<<") {
                    withAnimation {
                        reorderValues(.scrollToRight)
                    }
                }
                
                Button(">>>") {
                    withAnimation {
                        reorderValues(.scrollToLeft)
                    }
                }
            }
            .background(Color.green)
            .offset(y: 250)
            .zIndex(20)
            
        }
        
    }

改变位置信息

原理解析:现有位置是:

Slots[0] Slots[1] Slots[2] Slots[3] Slots[4]
hanfu0 hanfu1 hanfu2 hanfu3 hanfu4

当向左滚动时,最左个被挤出屏幕,回到最右的位置,此时的位置信息应该为:

Slots[0] Slots[1] Slots[2] Slots[3] Slots[4]
hanfu1 hanfu2 hanfu3 hanfu4 hanfu0

注意,记得把hanfu0 的zIndex设置为最小,这样才能实现从背后绕过的效果。

    enum ReorderMode {
        case scrollToLeft
        case scrollToRight
    }
    
    func reorderValues(_ mode: ReorderMode) {
        var keys:[String]  {
            myCells.map({$0.id})
        }
        switch mode {
        case .scrollToLeft:
            var newVRects = cellSlotDic
            var newZIndexs = cellZIndexDic
            for i in 0..<keys.count {
                let preI = i - 1 < 0 ? keys.count-1 : i - 1
                let rect = cellSlotDic[keys[i]]!
                newVRects[keys[preI]] = rect
                
                // 注意,值的改变是立即生效的,只是zIndex不会有动画,而位移和形变会有动画。
                // 所以,经过以上的重新排序,一旦给赋值vRects,在视图的渲染中,原本的最左已经跑到最右,为了让目标cell播放位移动画时,是从后边绕过的,因此设置最小的zIndex
                let theRightmostKey = rect.id == Slots.last!.id
                newZIndexs[keys[i]] = theRightmostKey  ? 1 : 3
            }
            
            cellZIndexDic = newZIndexs
            cellSlotDic = newVRects
            
        case .scrollToRight:
            var newVRects = cellSlotDic
            var newZIndexs = cellZIndexDic
            for i in 0..<keys.count {
                let nextI = i + 1 > keys.count-1 ? 0 : i + 1
                let rect = cellSlotDic[keys[i]]!
                newVRects[keys[nextI]] = rect
                

                let theLeftmostKey = rect.id == Slots.first!.id
                newZIndexs[keys[i]] = theLeftmostKey  ? 1 : 3
            }
            
            cellZIndexDic = newZIndexs
            cellSlotDic = newVRects
            
        }
    }
    

题外话

字典的数据互换,可不简洁。
一开始是使用数组对应每个位置,实现数组元素“平移”会比字典容易许多,代码也比上面简洁许多。
因为加入zIndex因素,换成字典的方式,比较不容易把自己绕晕。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章