SwiftUI极简教程26:构建一个Banner图片轮播(下)

1,043 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第26天,点击查看活动详情

今日职言:每一次金融危机都是蓄谋已久的精确定向爆破,熠熠夺目的崭新金融大厦总是建筑在成千上万破产者的废墟之上。

承接上一章的内容,我们继续实现下如何使用SwiftUI构建一个Banner轮播图。

上一章,我们使用HStack横向视图和Gestures手势做一个Banner轮播图,完成了基础的交互,但还不算全部完成。

这一章,我们将学习Banner轮播图的交互,包含移动Banner轮播图的动画,以及点击Banner轮播图进入详情页。

1.png

那么,我们开始吧。

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

2.png

为了效果好看,我们还可以调整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弹性旋转动画。

我们运行下模拟器预览下效果。

3.png

恭喜你,完成了Banner轮播图的动画效果!

DatailView详情页

下面,我们来完成下点击Banner轮播图进入DetailView详情页的交互。

首先创建一个新的页面,我们命名为DetailView.swift

4.png

下面,我们完成下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)
            }
        }
    }
}

5.png

交互逻辑

首先,我们先实现点击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)
}

6.png

当我们点击CardView卡片视图的时候,展示DetailView详情页。

7.png

当然,还远远不够,我们希望展示的效果是,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)

8.png

我们发现一个交互问题,现在我们尝试拖动Banner图片轮播,它也是可以拖动的,这不是我们想要的效果。

我们可以按照上面的逻辑,再用isShowDetailView判断一下。

//如果没有被点击
!self.isShowDetailView ? 
    //代码块
:nil

9.png

好了,这样,我们在展示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)

10.png

恭喜你,完成了所有的编程!

我们回顾一下,上篇我们完成了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)
            }
        }
    }
}

快来动手试试吧!

如果本专栏对你有帮助,不妨点赞、评论、关注~