SwiftUI五视图动画和转场

代码下载

使用SwiftUI可以把视图状态的改变转成动画过程,SwiftUI会处理所有复杂的动画细节。在这篇中,会给跟踪用户徒步的图表视图添加动画,使用animation(_:)修改器给一个视图添加动画效果非常容易。

下载起步项目并跟着本篇教程一步步实践,或者查看本篇完成状态时的工程代码去学习,项目文件。

添加 Hiking 数据到应用程序

在添加动画之前,需要一些东西来做动画。在本节中,将导入和建模 Hiking 数据,然后添加一些预构建的视图,以便在图中静态地显示该数据。
请添加图片描述

1、从下载文件的“Resources”文件夹将 hikeData.json 文件拖动放入项目。在单击 Finish 之前,请确保选择 “Copy items if need”。

2、新建 Hike.swift 文件,与Landmark结构体一样,Hike结构体也遵守Codable协议,并且具有与相应数据文件中的键匹配的属性:

import Foundation


struct Hike: Codable, Hashable, Identifiable {
    var id: Int
    var name: String
    var distance: Double
    var difficulty: Int
    var observations: [Observation]


    static var formatter = LengthFormatter()


    var distanceText: String {
        Hike.formatter
            .string(fromValue: distance, unit: .kilometer)
    }


    struct Observation: Codable, Hashable {
        var distanceFromStart: Double


        var elevation: Range<Double>
        var pace: Range<Double>
        var heartRate: Range<Double>
    }
}

3、新建 ModelData.swift 文件:

import Foundation

@Observable
class ModelData {
    var hikes: [Hike] = load("hikeData.json")
}

func load<T: Decodable>(_ filename: String) -> T {
    guard let path = Bundle.main.url(forResource: filename, withExtension: nil),
       let data = try? Data(contentsOf: path),
       let result = try? JSONDecoder().decode(T.self, from: data) else {
        fatalError("数据加载失败!")
    }
    
    return result
}

4、将已下载文件的Resources文件夹中的hike文件夹拖到项目中。在单击Finish之前,请确保选择“Copy items if need”和“Create groups”。熟悉这些新的视图,它们一起工作来显示加载到模型中的 hike 数据。

5、在HikeView.swift中,打开实时预览,体验一下图表的打开和隐藏,此时的状态改变时是没有添加动画效果的。在本篇的实践中,保持实时预览一直打开,每一步修改的效果就可以实时的看到。

给每个视图单独添加动画

在视图上使用animation(_:)修改器时,SwiftUI会在视图的任何可进行动画的属性发生改变时产生对应的动画效果。视图的颜色、不透明度、旋转角度、大小及一些其它属性都是可进行动画的。
请添加图片描述

1、在HikeView.swift中,给显示/隐藏切换的箭头按钮添加旋转动画,会发现现在按钮点击时的旋转有一个动画过渡的效果了。当视图从隐藏到展示时,让切换按钮变大1.5倍,把动画的类型从easeInOut改为spring()。SwiftUI包含一些预设或可自定义的动画类型,像弹簧(spring)动画和类型液体(fluid)动画类型。可以调整动画开始前的等待时长、动画的速度也可以指定让动画循环重复的进行:

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(hike: hike, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button {
                    showDetail.toggle()
                } label: {
                    Label("Graph", systemImage: "chevron.right.circle")
                        .labelStyle(.iconOnly)
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                        .animation(.spring())
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

2、如果只想让按钮具有缩放动画而不进行旋转动画,可以在scaleEffect前面添加animation(nil)来实现。可以在这里做一些实验,如果把其它的一些动画效果结合在一起,会怎么样:

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(hike: hike, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button {
                    showDetail.toggle()
                } label: {
                    Label("Graph", systemImage: "chevron.right.circle")
                        .labelStyle(.iconOnly)
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .animation(nil)
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                        .animation(.spring())
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

3、进行下一节之前,把本节中添加的animation(_:)修改器都去掉。

把视图的状态改态转化成动画效果

已经学会了给单个视图添加动画的方法,现在可以学习怎么在视图的状态发生改变时添加动画效果。当用户点击按钮时会切换showDetail状态的值,在视图变化过程中添加动画效果。
请添加图片描述

1、把showDetail.toggle()包裹在withAnimation函数调用块中。showDetail的改变影响了视图HikeDetail和详情切换按钮,在显示/隐藏详情的过程中都有了过滤动画效果。

2、放慢动画速度,可以观察SwiftUI动画在被中断下是怎么运作的。给withAnimation传入一个时长4秒的基本动画参数.easeInOut(duration:4),可以指定动画过程时长,给withAnimation传入的动画参数与.animation(_:)修改器可用参数一致。

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(hike: hike, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button {
                    withAnimation(.easeInOut(duration: 4)) {
                        showDetail.toggle()
                    }
                } label: {
                    Label("Graph", systemImage: "chevron.right.circle")
                        .labelStyle(.iconOnly)
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

3、在动画过程进行中点击按钮切换视图状态,查看对应的动画被中断时的效果。进行下一节之前,把动画时长参数(.easeInOut(duration: 4))去掉,让动画不再缓慢进行。

定制视图转场动画

默值情况下,视图离屏和入屏时的动画效果是渐隐/渐现, 这个默认的转场效果可以使用transition(_:)修改器进行定制。

1、给HikeView视图添加transition(_:)修改器,并定制转场参数为.slide,转场动画为滑入/滑出:

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(hike: hike, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button {
                    withAnimation {
                        showDetail.toggle()
                    }
                } label: {
                    Label("Graph", systemImage: "chevron.right.circle")
                        .labelStyle(.iconOnly)
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
                    .transition(.slide)
            }
        }
    }
}

2、可以把滑入/滑出这种转场动画封装起来,方便其它视图复用同样的转场效果:

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        .slide
    }
}

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(hike: hike, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button {
                    withAnimation {
                        showDetail.toggle()
                    }
                } label: {
                    Label("Graph", systemImage: "chevron.right.circle")
                        .labelStyle(.iconOnly)
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
                    .transition(AnyTransition.moveAndFade)
            }
        }
    }
}

3、在moveAndFade转场效果的定义中使用move(edge:),让滑入/滑出从屏幕的同一边进行:

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        .move(edge: .trailing)
    }
}

4、使用asymmetric(insertion:removal:)修改器来定制视图显示/消失时的转场动画效果:

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        let insertion = self.move(edge: .trailing)
            .combined(with: .opacity)
        let removal = self.scale
            .combined(with: .opacity)
        
        return self.asymmetric(insertion: insertion, removal: removal)
    }
}

组合复杂的动画效果

点击图表下面的三个按钮,会在三个不同的数据集间进行切换并展示。本节中会使用组合动画,让图表在不同数据集间切换时的转换动画流畅自然。
请添加图片描述
1、把showDetail的默认值改为true,并把HikeView的预览模式视图固定在画布上。这样可以在编辑其它文件时,依然看到动画效果的变化。

2、在HikeGraph.swift中定义了一个新的波动动画,并把它与滑入/滑出动画一起应用到图表视图上:

extension Animation {
    static func ripple() -> Animation {
        Animation.default
    }
}

struct HikeGraph: View {
    var hike: Hike
    var path: KeyPath<Hike.Observation, Range<Double>>

    var color: Color {
        switch path {
        case \.elevation:
            return .gray
        case \.heartRate:
            return Color(hue: 0, saturation: 0.5, brightness: 0.7)
        case \.pace:
            return Color(hue: 0.7, saturation: 0.4, brightness: 0.7)
        default:
            return .black
        }
    }

    var body: some View {
        let data = hike.observations
        let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: path] })
        let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()!
        let heightRatio = 1 - CGFloat(maxMagnitude / magnitude(of: overallRange))

        return GeometryReader { proxy in
            HStack(alignment: .bottom, spacing: proxy.size.width / 120) {
                ForEach(Array(data.enumerated()), id: \.offset) { index, observation in
                    GraphCapsule(
                        index: index,
                        color: color,
                        height: proxy.size.height,
                        range: observation[keyPath: path],
                        overallRange: overallRange
                    ).animation(.ripple())
                }
                .offset(x: 0, y: proxy.size.height * heightRatio)
            }
        }
    }
}

3、把动画切换为弹簧动画(spring),并设置弹簧阻尼系数为0.5,动画过程中产生了逐渐回弹效果:

extension Animation {
    static func ripple() -> Animation {
        Animation.spring(dampingFraction: 0.5)
    }
}

4、加速弹簧动画的执行速度,缩短切换图表的时间:

extension Animation {
    static func ripple() -> Animation {
        Animation.spring(dampingFraction: 0.5)
            .speed(2)
    }
}

5、以当条形在图表中的位置为参数,添加延迟效果,图表中的每个条形会顺序动起来:

extension Animation {
    static func ripple(index: Int) -> Animation {
        Animation.spring(dampingFraction: 0.5)
            .speed(2)
            .delay(Double(index)*0.03)
    }
}

struct HikeGraph: View {
    var hike: Hike
    var path: KeyPath<Hike.Observation, Range<Double>>

    var color: Color {
        switch path {
        case \.elevation:
            return .gray
        case \.heartRate:
            return Color(hue: 0, saturation: 0.5, brightness: 0.7)
        case \.pace:
            return Color(hue: 0.7, saturation: 0.4, brightness: 0.7)
        default:
            return .black
        }
    }

    var body: some View {
        let data = hike.observations
        let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: path] })
        let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()!
        let heightRatio = 1 - CGFloat(maxMagnitude / magnitude(of: overallRange))

        return GeometryReader { proxy in
            HStack(alignment: .bottom, spacing: proxy.size.width / 120) {
                ForEach(Array(data.enumerated()), id: \.offset) { index, observation in
                    GraphCapsule(
                        index: index,
                        color: color,
                        height: proxy.size.height,
                        range: observation[keyPath: path],
                        overallRange: overallRange
                    ).animation(.ripple(index: index))
                }
                .offset(x: 0, y: proxy.size.height * heightRatio)
            }
        }
    }
}

观察一下自定义波动(rippling)效果是怎么作用在视图转场中的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/693389.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Python】Selenium基础入门

Selenium基础入门 一、Selenium简介二、Selenium的安装三、Selenium的使用1.访问web网站2.元素定位根据标签 id 获取元素根据标签 name 属性的值获取元素根据 Xpath 语句获取元素根据标签名获取元素根据CSS选择器获取元素根据标签的文本获取元素&#xff08;精确定位&#xff0…

学习使用 Frida 过程中出现的问题

一、adb shell命令报错&#xff1a;error: no devices found 目前该问题解决方法仅供参考&#xff0c;可先看看再选择试试&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 查看此电脑也会发现没有出现手机型号文件夹。 第一步&#xff1a; 检查一下手机开了u…

Nginx(title小图标)修改方法

本章主要讲述Nginx如何上传网站图标。 操作系统&#xff1a; CentOS Stream 9 首先我们bing搜索ico网站图标在线设计&#xff0c;找到喜欢的设计分格并下载。 是一个压缩包 然后我们上传到nginx解压 [rootlocalhost html]# rz[rootlocalhost html]# unzip favicon_logosc.z…

第R3周:天气预测

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客 &#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 &#x1f680; 文章来源&#xff1a;K同学的学习圈子 目录 我的环境 语言环境&#xff1a;python3.8.18编译器&#xff1a;jupyter not…

MAVEN架构项目管理工具

1、什么是maven Maven是跨平台的项目管理工具。主要服务于基于Java平台的项目构建&#xff0c;依赖管理和项目信息管理。 2、maven的目标&#xff1a;Maven的主要目标是为了使开发人员在最短的时间内领会项目的所有状态 3、使用maven不需要考虑各个依赖的版本&#xff0c;因…

如何使用共享GPU平台搭建LLAMA3环境(LLaMA-Factory)

0. 简介 最近受到优刻得的使用邀请&#xff0c;正好解决了我在大模型和自动驾驶行业对GPU的使用需求。UCloud云计算旗下的[Compshare](https://www.compshare.cn/? ytagGPU_lovelyyoshino_Lcsdn_csdn_display)的GPU算力云平台。他们提供高性价比的4090 GPU&#xff0c;按时收…

Django 鸡与蛋问题

"Django 的鸡与蛋问题"通常指的是在开始 Django 项目时&#xff0c;你可能会遇到的一个困境&#xff1a;是先设计数据库模型还是先编写视图和控制器&#xff08;即视图函数&#xff09;&#xff1f; 这个问题的实质是在于&#xff0c;Django 的核心部分是由数据库模…

PDF转图片工具

背景&#xff1a; 今天有个朋友找我&#xff1a;“我有个文件需要更改&#xff0c;但是文档是PDF的&#xff0c;需要你帮我改下内容&#xff0c;你是搞软件的&#xff0c;这个对你应该是轻车熟路了吧&#xff0c;帮我弄弄吧”&#xff0c;听到这话我本想反驳&#xff0c;我是开…

python - Pandas缺失值处理

文中所用数据集已上传,找不到的可以私聊我 学习目标 知道空值和缺失值的区别以及缺失值的影响 知道如何查看数据集缺失值情况的方法 知道缺失值处理的办法 1 NaN简介 好多数据集都含缺失数据。缺失数据有多种表现形式 数据库中&#xff0c;缺失数据表示为NULL 在某些编程语…

LeetCode338比特位计数

题目描述 给你一个整数 n &#xff0c;对于 0 < i < n 中的每个 i &#xff0c;计算其二进制表示中 1 的个数 &#xff0c;返回一个长度为 n 1 的数组 ans 作为答案。 解析 动态规划&#xff0c;将当前的数的最后一位去掉&#xff0c;然后判断去掉的最后一位是0还是1。…

pip(3) install,完美解决 externally-managed-environment

前言 现象 在 Manjaro 22、Ubuntu 23.04、Fedora 38 等最新的linux发行版中运行pip install时&#xff0c;通常会收到一个错误提示&#xff1a;error: externally-managed-environment&#xff0c;即“外部管理环境”错误&#xff0c;但这不是一个 bug。 如果您想阅读&#x…

Chrome浏览器书签同步不及时怎么办?两种方法帮你解决!

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;CSDN博客专家   &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01…

9.7 Go语言入门(映射 Map)

Go语言入门&#xff08;映射 Map&#xff09; 目录六、映射 Map1. 声明和初始化映射1.1 使用 make 函数1.2 使用映射字面量 2. 映射的基本操作2.1 插入和更新元素2.2 访问元素2.3 检查键是否存在2.4 删除元素2.5 获取映射的长度 3. 遍历映射4. 映射的注意事项4.1 映射的零值4.2…

Foxmail邮箱的使用方法和功能最全介绍

Foxmail邮箱是我们办公邮箱中比较有代表性和使用性的一款邮箱软件&#xff0c;今天笔者为大家介绍一下Foxmail邮箱的功能和使用方法。 1、首先我们从安装Foxmail邮箱开始 2、点击安装等待安装成功 3、双击打开 &#xff0c;出现邮箱设置界面输入我们的账号密码&#xff0c;点击…

Elasticsearch 管道查询语言 ES|QL 现已正式发布

作者&#xff1a;Costin Leau, George Kobar 今天&#xff0c;我们很高兴地宣布 ES|QL&#xff08;Elasticsearch 查询语言&#xff09;全面上市&#xff0c;这是一种从头开始设计的动态语言&#xff0c;用于转换、丰富和简化数据调查。在新的查询引擎的支持下&#xff0c;ES|Q…

【JAVASE】详讲JAVA语法

这篇你将收获到以下知识&#xff1a; &#xff08;1&#xff09;方法重载 &#xff08;2&#xff09;方法签名 一&#xff1a;方法重载 什么是方法重载&#xff1f; 在一个类中&#xff0c;出现了多个方法的名称相同&#xff0c;但是它们的形参列表是不同的&#xff0c;那…

Transparent 且 Post-quantum zkSNARKs

1. 引言 前序博客有&#xff1a; SNARK原理示例SNARK性能及安全——Prover篇SNARK性能及安全——Verifier篇 上图摘自STARKs and STARK VM: Proofs of Computational Integrity。 上图选自&#xff1a;Dan Boneh 斯坦福大学 CS251 Fall 2023 Building a SNARK 课件。 SNARK…

逻辑这回事(四)----时序分析与时序优化

基本时序参数 图1.1 D触发器结构 图1.2 D触发器时序 时钟clk采样数据D时&#xff0c;Tsu表示数据前边沿距离时钟上升沿的时间&#xff0c;MicTsu表示时钟clk能够稳定采样数据D的所要求时间&#xff0c;Th表示数据后边沿距离时钟上升沿的时间&#xff0c;MicTh表示时钟clk采样…

C语言王国——数据的内存管理

目录 一、引言 二、整形在内存中的存储 2.1 进制之间的转换 2.1.1 整形的二进制 2.1.2 十进制和二进制 2.1.3 十进制和八进制的转换 2.1.4 十六进制和十进制的转换 2.2 原码&#xff0c;反码&#xff0c;和补码 三、大、小端字节序 3.1 大小端的定义 3.2 为什么会有大…

【Linux进程篇】Linux中的等待机制与替换策略

W...Y的主页 &#x1f60a; 代码仓库分享&#x1f495; 目录 ​编辑 进程等待 进程等待必要性 进程等待的方法 wait方法 waitpid方法 获取子进程status 阻塞与非阻塞 进程程序替换 替换原理 替换函数 进程等待 进程等待必要性 之前讲过&#xff0c;子进程退出&am…