1. 程式人生 > 其它 >SwiftUI 一種旋轉木馬效果的實現(Carousel)

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