承接上一章的内容,我们继续实现下如何使用SwiftUI
构建一个Banner
轮播图。
上一章,我们使用HStack
横向视图和Gestures
手势做一个Banner
轮播图,完成了基础的交互,但还不算全部完成。
这一章,我们将学习Banner
轮播图的交互,包含移动Banner
轮播图的动画,以及点击Banner
轮播图进入详情页。
那么,我们开始吧。
Animation动画效果
我们通过GeometryReader
几何视图的outerView
设置了CardView
卡片的大小,但它是固定的。
.frame(width: outerView.size.width, height: outerView.size.height)
我们了解下Banner
轮播图的展示逻辑,它是当前显示的CardView
卡片会大一些,切换的时候,另外的会小一些,当我们将卡片滑动到中间展示
时,它又会放大。
我们要做的就是这个效果。
.frame(width: outerView.size.width, height: self.currentIndex == index ? 250 : 200)
我们可以尝试根据currentIndex
当前索引位置来控制CardView
卡片的高度,如果它在当前,那么height
高度为250
,如果不是,height
高度为200
。
为了效果好看,我们还可以调整CardView
卡片的透明度,不在当前展示的卡片,我们让它“模糊
”一点,突出中间的卡片。
.opacity(self.currentIndex == index ? 1.0 : 0.7) 复制代码
最后,我们把动画效果加到整个GeometryReader
几何视图中。
.animation(.interpolatingSpring(mass: 0.6, stiffness: 100, damping: 10, initialVelocity: 0.3),value: offset) 复制代码
我们开启了动画,动画呈现的方式为interpolatingSpring
弹性旋转动画。
我们运行下模拟器预览下效果。
恭喜你,完成了Banner
轮播图的动画效果!
DatailView详情页
下面,我们来完成下点击Banner
轮播图进入DetailView
详情页的交互。
首先创建一个新的页面,我们命名为DetailView.swift
。
下面,我们完成下DetailView
页面的设计,它由一个标题、内容和按钮组成。
struct DetailView: View { let imageName: String var body: some View { GeometryReader { geometry in ScrollView { VStack(alignment: .leading, spacing: 5) { // 图片名称 Text(self.imageName) .font(.system(.title, design: .rounded)) .fontWeight(.heavy) .padding(.bottom, 30) // 描述文字 Text("要想在一个生活圈中生活下去,或者融入职场的氛围,首先你要学习这个圈子的文化和发展史,并尝试用这个圈子里面的“话术”和他们交流,这样才能顺利地融入这个圈子。") .padding(.bottom, 40) // 按钮 Button(action: { }) { Text("知道了") .font(.system(.headline, design: .rounded)) .fontWeight(.heavy) .foregroundColor(.white) .padding() .frame(minWidth: 0, maxWidth: .infinity) .background(Color.blue) .cornerRadius(8) } } .padding() .frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading) .background(Color.white) .cornerRadius(15) } } } }
交互逻辑
首先,我们先实现点击CardView
打开DetailView
详情页。
我们使用GeometryReader
几何视图和ScrollView
滚动视图搭建了一个DetailView
详情页。
然后我们回到ContentView
首页,创建一个点击
状态。
@State var isShowDetailView = false
当我们点击CardView
卡片时,进入到对应的详情页。和之前的章节一样我们在CardView
卡片视图上添加点击事件,然后用ZStack
层叠视图将DetailView
详情页和ContentView
首页叠加在一起。
//详情页 if self.isShowDetailView { DetailView(imageName: imageModels[currentIndex].imageName) .offset(y: 200) .transition(.move(edge: .bottom)) .animation(.interpolatingSpring(mass: 0.5, stiffness: 100, damping: 10, initialVelocity: 0.3),value: offset) }
当我们点击CardView
卡片视图的时候,展示DetailView
详情页。
当然,还远远不够,我们希望展示的效果是,Banner
图片轮播在展示详情的时候,背景部分可以看到原先Banner
轮播的图片,我们可以根据isShowDetailView
的状态再调整下样式。
//如果点击就图片就移上去 .offset(y: self.isShowDetailView ? -innerView.size.height * 0.3 : 0) //如果点击图片两边就不留边距 .padding(.horizontal, self.isShowDetailView ? 0 : 20) //如果点击就图片调整大小 .frame(width: outerView.size.width, height: self.currentIndex == index ? (self .isShowDetailView ? outerView.size.height : 250) : 200)
我们发现一个交互问题,现在我们尝试拖动Banner
图片轮播,它也是可以拖动的,这不是我们想要的效果。
我们可以按照上面的逻辑,再用isShowDetailView
判断一下。
//如果没有被点击 !self.isShowDetailView ? //代码块 :nil
好了,这样,我们在展示DetailView
详情页时,就不用担心Banner
轮播图被拖动了。
我们最后再加上一个关闭按钮,用于关闭DetailView
详情页。
//关闭按钮 Button(action: { self.isShowDetailView = false }) { Image(systemName: "xmark.circle.fill") .font(.system(size: 30)) .foregroundColor(.black) .opacity(0.7) .contentShape(Rectangle()) } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topTrailing) .padding(.trailing)
恭喜你,完成了所有的编程!
我们回顾一下,上篇我们完成了ScrollView
滚动视图创建Banner
轮播图,后来我们发现不太可行。然后,中篇我们尝试使用HStack
横向视图和Gestures
手势做一个Banner
轮播图,并完成了基本的交互。下篇我们继续完成了整个Banner
轮播图的交互逻辑。
真心不容易啊。
ContentView完整代码
struct ContentView: View { @State var currentIndex = 0 @GestureState var dragOffset: CGFloat = 0 @State private var offset: CGFloat = .zero @State var isShowDetailView = false var body: some View { ZStack { //首页轮播图 GeometryReader { outerView in HStack(spacing: 0) { ForEach(imageModels.indices, id: \.self) { index in GeometryReader { innerView in CardView(image: imageModels[index].image, imageName: imageModels[index].imageName) //如果点击就图片就移上去 .offset(y: self.isShowDetailView ? -innerView.size.height * 0.3 : 0) } //如果点击图片两边就不留边距 .padding(.horizontal, self.isShowDetailView ? 0 : 20) .opacity(self.currentIndex == index ? 1.0 : 0.7) //如果点击就图片调整大小 .frame(width: outerView.size.width, height: self.currentIndex == index ? (self .isShowDetailView ? outerView.size.height : 250) : 200) //点击进入详情页 .onTapGesture { self.isShowDetailView = true } } } .frame(width: outerView.size.width, height: outerView.size.height, alignment: .leading) .offset(x: -CGFloat(self.currentIndex) * outerView.size.width) .offset(x: self.dragOffset) // 拖动事件 .gesture( //如果没有被点击 !self.isShowDetailView ? DragGesture() .updating(self.$dragOffset, body: { value, state, transaction in state = value.translation.width }) .onEnded({ value in let threshold = outerView.size.width * 0.65 var newIndex = Int(-value.translation.width / threshold) + self.currentIndex newIndex = min(max(newIndex, 0), imageModels.count - 1) self.currentIndex = newIndex }) : nil ) } .animation(.interpolatingSpring(mass: 0.6, stiffness: 100, damping: 10, initialVelocity: 0.3),value: offset) //详情页 if self.isShowDetailView { DetailView(imageName: imageModels[currentIndex].imageName) .offset(y: 200) .transition(.move(edge: .bottom)) .animation(.interpolatingSpring(mass: 0.5, stiffness: 100, damping: 10, initialVelocity: 0.3),value: offset) //关闭按钮 Button(action: { self.isShowDetailView = false }) { Image(systemName: "xmark.circle.fill") .font(.system(size: 30)) .foregroundColor(.black) .opacity(0.7) .contentShape(Rectangle()) } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topTrailing) .padding(.trailing) } } } }
快来动手试试吧!