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 、zIndex对应起来。
显示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 |
注意,zIndex的变化没有动画,可以理解为在视觉上是立即实现的。记得把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因素,换成字典的方式,比较不容易把自己绕晕。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现