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因素,換成字典的方式,比較不容易把自己繞暈。

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