SwiftUI使用Paths和AnimatableData实现酷炫的颜色切换动画

老铁们,是时候燥起来了!本文中我们将学习如何使用 SwiftUI 中的 PathsAnimatableData 来制作颜色切换动画。

这些快速切换的动画是怎么实现的呢?让我们来看下文吧!

基础

要实现动画的关键是在 SwiftUI 中创建一个实现 Shape 协议的结构体。我们把它命名为 SplashShape 。在 Shape 协议中,有一个方法叫做 path(in rect: CGRect) -> Path ,这个方法可以用来设置图形的外观。我们就用这个方法来实现本文中的各种动画。

创建 SplashShape 结构体

下面我们创建一个叫做 SplashStruct 的结构体,它继承于 Shape 协议。

import SwiftUI

struct SplashShape: Shape {

 func path(in rect: CGRect) -> Path {
 return Path()
 }
}

我们首先创建两种动画类型: leftToRightrightToLeft ,效果如下所示:

Splash 动画

我们创建一个名为 SplashAnimation枚举 来定义动画类型,便于以后更方便地扩展新动画(文章末尾可以验证!)。

import SwiftUI

struct SplashShape: Shape {

 public enum SplashAnimation {
 case leftToRight
 case rightToleft
 }

 func path(in rect: CGRect) -> Path {
 return Path()
 }
}

path() 方法中,我们可以选择需要使用的动画,并且返回动画的 Path 。但是首先,我们必须创建变量来存储动画类型,记录动画过程。

import SwiftUI

struct SplashShape: Shape {

 public enum SplashAnimation {
 case leftToRight
 case rightToleft
 }

 var progress: CGFloat
 var animationType: SplashAnimation

 func path(in rect: CGRect) -> Path {
 return Path()
 }
}

progress 的取值范围在 0 和 1 之间,它代表整个动画的完成进度。当我们编写 path() 方法时,它就会派上用场。

编写 path() 方法

跟之前说的一样,为了返回正确的 Path ,我们需要明确正在使用哪一种动画。在 path() 方法中编写 switch 语句,并且用上我们之前定义的 animationType 。

func path(in rect: CGRect) -> Path {
 switch animationType {
 case .leftToRight:
  return Path()
 case .rightToLeft:
  return Path()
 }
}

现在这个方法只会返回空 paths。我们需要创建产生真实动画的方法。

实现动画方法

在 path() 方法的下面,创建两个新的方法: leftToRight() 和 rightToLeft() ,每个方法表示一种动画类型。在每个方法体内,我们会创建一个矩形形状的 Path ,它会根据 progress 变量的值随时间发生变换。

func leftToRight(rect: CGRect) -> Path {
 var path = Path()
 path.move(to: CGPoint(x: 0, y: 0)) // Top Left
 path.addLine(to: CGPoint(x: rect.width * progress, y: 0)) // Top Right
 path.addLine(to: CGPoint(x: rect.width * progress, y: rect.height)) // Bottom Right
 path.addLine(to: CGPoint(x: 0, y: rect.height)) // Bottom Left
 path.closeSubpath() // Close the Path
 return path
}

func rightToLeft(rect: CGRect) -> Path {
 var path = Path()
 path.move(to: CGPoint(x: rect.width, y: 0))
 path.addLine(to: CGPoint(x: rect.width - (rect.width * progress), y: 0))
 path.addLine(to: CGPoint(x: rect.width - (rect.width * progress), y: rect.height))
 path.addLine(to: CGPoint(x: rect.width, y: rect.height))
 path.closeSubpath()
 return path
}

然后在 path() 方法中调用上面两个新方法。

func path(in rect: CGRect) -> Path {
 switch animationType {
 case .leftToRight:
  return leftToRight(rect: rect)
 case .rightToLeft:
  return rightToLeft(rect: rect)
 }
}

动画数据

为了确保 Swift 知道在更改 progress 变量时如何对 Shape 进行动画处理,我们需要指定一个响应动画的变量。在 progress 和 animationType 变量下面,定义 animatableData 。这是一个基于 Animatable 协议 的变量,它可以通知 SwiftUI 在数据改变时,对视图进行动画处理。

var progress: CGFloat
var animationType: SplashAnimation

var animatableData: CGFloat {
 get { return progress }
 set { self.progress = newValue}
}

颜色切换时产生动画

到目前为止,我们已经创建了一个 Shape ,它将随着时间的变化而变化。接下来,我们需要将它添加到视图中,并在视图颜色改变时自动对其进行动画处理。这时候我们引入 SplashView 。我们将创建一个 SplashView 来自动更新 SplashShape 的 progress 变量。当 SplashView 接收到新的 Color 时,它将触发动画。

首先,我们创建 SplashView 结构体。

import SwiftUI

struct SplashView: View {

 var body: some View {
 // SplashShape Here
 }
}

SplashShape 需要使用 SplashAnimation 枚举作为参数,所以我们会把它作为参数传递给 SplashView 。另外,我们要在视图的背景颜色变化时设置动画,所以我们也要传递 Color 参数。这些细节会在我们的初始化方法中详细说明。

ColorStore 是自定义的 ObservableObject。它用来监听 SplashView 结构体中 Color 值的改变,以便我们可以初始化 SplashShape 动画,并最终改变背景颜色。我们稍后展示它的工作原理。

struct SplashView: View {

 var animationType: SplashShape.SplashAnimation
 @State private var prevColor: Color // Stores background color
 @ObservedObject var colorStore: ColorStore // Send new color updates

 init(animationType: SplashShape.SplashAnimation, color: Color) {
 self.animationType = animationType
 self._prevColor = State<Color>(initialValue: color)
 self.colorStore = ColorStore(color: color)
 }

 var body: some View {
 // SplashShape Here
 }

}

class ColorStore: ObservableObject {
 @Published var color: Color

 init(color: Color) {
 self.color = color
 }
}

构建 SplashView body

在 body 内部,我们需要返回一个 Rectangle ,它和 SplashView 当前的颜色保持一致。然后使用之前定义的 ColorStore ,以便于我们接收更新的颜色值来驱动动画。

var body: some View {
 Rectangle()
 .foregroundColor(self.prevColor) // Current Color
 .onReceive(self.colorStore.$color) { color in
  // Animate Color Update Here
 }
}

当颜色改变时,我们需要记录 SplashView 中正在改变的颜色和进度。为此,我们定义 layers 变量。

@State var layers: [(Color,CGFloat)] = [] // New Color & Progress

现在回到 body 变量内部,我们给 layers 变量添加新接收的 Colors 。添加的时候我们把进度设置为 0 。然后,在半秒之内的动画过程中,我们把进度设置为 1 。

var body: some View {
 Rectangle()
 .foregroundColor(self.prevColor) // Current Color
 .onReceive(self.colorStore.$color) { color in
  // Animate Color Update Here
  self.layers.append((color, 0))

  withAnimation(.easeInOut(duration: 0.5)) {
  self.layers[self.layers.count-1].1 = 1.0
  }
 }
}

现在在这段代码中, layers 变量中添加了更新后的颜色,但是颜色并没有展示出来。为了展示颜色,我们需要在 body 变量内部为 Rectangle 的每一个图层添加一个覆盖层。

var body: some View {
 Rectangle()
 .foregroundColor(self.prevColor)
 .overlay(
  ZStack {
  ForEach(layers.indices, id: \.self) { x in
   SplashShape(progress: self.layers[x].1, animationType: self.animationType)
   .foregroundColor(self.layers[x].0)
  }

  }

  , alignment: .leading)
 .onReceive(self.colorStore.$color) { color in
  // Animate color update here
  self.layers.append((color, 0))

  withAnimation(.easeInOut(duration: 0.5)) {
  self.layers[self.layers.count-1].1 = 1.0
  }
 }
}

测试效果

你可以在模拟器中运行下面的代码。这段代码的意思是,当你点击 ContentView 中的按钮时,它会计算 index 来选择 SplashView 中的颜色,同时也会触发 ColorStore 内部的更新。所以,当 SplashShape 图层添加到 SplashView 时,就会触发动画。

import SwiftUI

struct ContentView: View {
 var colors: [Color] = [.blue, .red, .green, .orange]
 @State var index: Int = 0

 @State var progress: CGFloat = 0
 var body: some View {
 VStack {

  SplashView(animationType: .leftToRight, color: self.colors[self.index])
  .frame(width: 200, height: 100, alignment: .center)
  .cornerRadius(10)
  .shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 4)

  Button(action: {
  self.index = (self.index + 1) % self.colors.count
  }) {
  Text("Change Color")
  }
  .padding(.top, 20)
 }

 }
}

还没有完成!

我们还有一个功能没实现。现在我们持续地把图层添加到 SplashView 上,但是没有删除它们。因此,我们需要在动画完成时把这些图层清理掉。

在 SplashView 结构体 body 变量的 onReceive() 方法内部,做如下改变:

.onReceive(self.colorStore.$color) { color in
 self.layers.append((color, 0))

 withAnimation(.easeInOut(duration: 0.5)) {
 self.layers[self.layers.count-1].1 = 1.0
 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  self.prevColor = self.layers[0].0 // Finalizes background color of SplashView
  self.layers.remove(at: 0) // removes itself from layers array
 }
 }
}

这行代码能让我们删除 layers 数组中使用过的值,并确保 SplashView 基于最新更新的值显示正确的背景色。

展示成果!

GitHub 源码

您可以在我的 Github 上查看本教程的源代码 !除了显示的示例外,还包括 SplashShape 和 SplashView 的完整源代码。 ....但是等等,还有更多!

彩蛋!

如果你熟悉我之前的教程,你应该了解我喜欢彩蛋 :wink:。在本文开头,我说过会实现更多动画。此刻终于来了…… 击鼓 ……。

Splash 动画

哈哈哈!!还记得吗?我说过会添加更多动画种类。

enum SplashAnimation {
 case leftToRight
 case rightToLeft
 case topToBottom
 case bottomToTop
 case angle(Angle)
 case circle
}
func path(in rect: CGRect) -> Path {

 switch self.animationType {
 case .leftToRight:
  return leftToRight(rect: rect)
 case .rightToLeft:
  return rightToLeft(rect: rect)
 case .topToBottom:
  return topToBottom(rect: rect)
 case .bottomToTop:
  return bottomToTop(rect: rect)
 case .angle(let splashAngle):
  return angle(rect: rect, angle: splashAngle)
 case .circle:
  return circle(rect: rect)
 }

}

你肯定会想…… “哇, 彩蛋也太多了……” 。不必苦恼。我们只需要在 SplashShape 的 path() 方法中添加几个方法,就能搞定。

下面我们逐个动画来搞定……

topToBottom 和 bottomToTop 动画

这些方法与 leftToRight 和 rightToLeft 非常相似,它们从 shape 的底部或顶部开始创建 path ,并使用 progress 变量随时间对其进行变换。

func topToBottom(rect: CGRect) -> Path {
 var path = Path()
 path.move(to: CGPoint(x: 0, y: 0))
 path.addLine(to: CGPoint(x: rect.width, y: 0))
 path.addLine(to: CGPoint(x: rect.width, y: rect.height * progress))
 path.addLine(to: CGPoint(x: 0, y: rect.height * progress))
 path.closeSubpath()
 return path
}

func bottomToTop(rect: CGRect) -> Path {
 var path = Path()
 path.move(to: CGPoint(x: 0, y: rect.height))
 path.addLine(to: CGPoint(x: rect.width, y: rect.height))
 path.addLine(to: CGPoint(x: rect.width, y: rect.height - (rect.height * progress)))
 path.addLine(to: CGPoint(x: 0, y: rect.height - (rect.height * progress)))
 path.closeSubpath()
 return path
}

circle 动画

如果你还记得小学几何知识,就应该了解勾股定理。 a^2 + b^2 = c^2

a 和 b 可以视为矩形的 高度 和 宽度 ,我们能够根据它们求得 c ,即覆盖整个矩形所需的圆的半径。我们以此为基础构建圆的 path,并使用 progress 变量随时间对它进行变换。

func circle(rect: CGRect) -> Path {
 let a: CGFloat = rect.height / 2.0
 let b: CGFloat = rect.width / 2.0

 let c = pow(pow(a, 2) + pow(b, 2), 0.5) // a^2 + b^2 = c^2 --> Solved for 'c'
 // c = radius of final circle

 let radius = c * progress
 // Build Circle Path
 var path = Path()
 path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: true)
 return path
}

angle 动画

这个动画知识点有点多。你需要使用切线计算角度的斜率,然后根据这个斜率创建一条直线。在矩形上移动这条直线时,根据它来绘制一个直角三角形。参见下图,各种彩色的线表示该线随时间移动时,覆盖整个矩形的状态。

方法如下:

func angle(rect: CGRect, angle: Angle) -> Path {

 var cAngle = Angle(degrees: angle.degrees.truncatingRemainder(dividingBy: 90))

 // Return Path Using Other Animations (topToBottom, leftToRight, etc) if angle is 0, 90, 180, 270
 if angle.degrees == 0 || cAngle.degrees == 0 { return leftToRight(rect: rect)}
 else if angle.degrees == 90 || cAngle.degrees == 90 { return topToBottom(rect: rect)}
 else if angle.degrees == 180 || cAngle.degrees == 180 { return rightToLeft(rect: rect)}
 else if angle.degrees == 270 || cAngle.degrees == 270 { return bottomToTop(rect: rect)}

 // Calculate Slope of Line and inverse slope
 let m = CGFloat(tan(cAngle.radians))
 let m_1 = pow(m, -1) * -1
 let h = rect.height
 let w = rect.width

 // tan (angle) = slope of line
 // y = mx + b ---> b = y - mx ~ 'b' = y intercept
 let b = h - (m_1 * w) // b = y - (m * x)

 // X and Y coordinate calculation
 var x = b * m * progress
 var y = b * progress

 // Triangle Offset Calculation
 let xOffset = (angle.degrees > 90 && angle.degrees < 270) ? rect.width : 0
 let yOffset = (angle.degrees > 180 && angle.degrees < 360) ? rect.height : 0

 // Modify which side the triangle is drawn from depending on the angle
 if angle.degrees > 90 && angle.degrees < 180 { x *= -1 }
 else if angle.degrees > 180 && angle.degrees < 270 { x *= -1; y *= -1 }
 else if angle.degrees > 270 && angle.degrees < 360 { y *= -1 }

 // Build Triangle Path
 var path = Path()
 path.move(to: CGPoint(x: xOffset, y: yOffset))
 path.addLine(to: CGPoint(x: xOffset + x, y: yOffset))
 path.addLine(to: CGPoint(x: xOffset, y: yOffset + y))
 path.closeSubpath()
 return path

}

总结

到此这篇关于SwiftUI使用Paths和AnimatableData实现酷炫的颜色切换动画的文章就介绍到这了,更多相关SwiftUI 颜色切换动画内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Swift开发之UITableView状态切换效果

    效果 源码 https://github.com/YouXianMing/Swift-Animations // // TableViewTapAnimationController.swift // Swift-Animations // // Created by YouXianMing on 16/8/7. // Copyright © 2016年 YouXianMing. All rights reserved. // import UIKit class TableViewTapAni

  • SwiftUI使用Paths和AnimatableData实现酷炫的颜色切换动画

    老铁们,是时候燥起来了!本文中我们将学习如何使用 SwiftUI 中的 Paths 和 AnimatableData 来制作颜色切换动画. 这些快速切换的动画是怎么实现的呢?让我们来看下文吧! 基础 要实现动画的关键是在 SwiftUI 中创建一个实现 Shape 协议的结构体.我们把它命名为 SplashShape .在 Shape 协议中,有一个方法叫做 path(in rect: CGRect) -> Path ,这个方法可以用来设置图形的外观.我们就用这个方法来实现本文中的各种动画. 创

  • JavaWeb文件上传下载实例讲解(酷炫的文件上传技术)

    一.课程概述 在Web应用系统开发中,文件上传功能是非常常用的功能,今天来主要讲讲JavaWeb中的文件上传功能的相关技术实现,并且随着互联网技术的飞速发展,用户对网站的体验要求越来越高,在文件上传功能的技术上也出现许多创新点,例如异步上传文件,拖拽式上传,黏贴上传,上传进度监控,文件缩略图,大文件断点续传,大文件秒传等等. 本课程需要的基础知识: 了解基本的Http协议内容 基本IO流操作技术 Servlet基础知识 javascript/jQuery技术基础知识 二.文件上传的基础 对于文件

  • Asp.net使用SignalR实现酷炫端对端聊天功能

    一.引言 在前一篇文章已经详细介绍了SignalR了,并且简单介绍它在Asp.net MVC 和WPF中的应用.在上篇博文介绍的都是群发消息的实现,然而,对于SignalR是为了实时聊天而生的,自然少了不像QQ一样的端对端的聊天了.本篇博文将介绍如何使用SignalR来实现类似QQ聊天的功能. 二.使用SignalR实现端对端聊天的思路 在介绍具体实现之前,我先来介绍了使用SignalR实现端对端聊天的思路.相信大家在前篇文章已经看到过Clients.All.sendMessage(name,

  • 基于RxJava实现酷炫启动页

    前言 RxJava 在 GitHub 主页上的自我介绍是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一个在 Java VM 上使用可观测的序列来组成异步的.基于事件的程序的库).这就是 RxJava ,概括得非常精准. 之前注意到coding APP启动页很是酷炫,今天我们使用RxJava和属性动画模仿实现其效果.

  • 酷炫!趣味十足的Linux命令

    1. pv 命令 有时候我们在电影屏幕上看到一些字幕一个个匀速显示出来,像有人在边敲键盘,边显示一样.Linux上的pv命令可以实现这种效果. 默认情况下,Linux是没有pv命令的,需要自行安装. 首先安装命令: # yum install pv [On RedHat based Systems] # sudo apt-get install pv [On Debian based Systems] 现在运行如下命令: 复制代码 代码如下: $ echo "Tecmint[dot]com is

  • 通过JQuery实现win8一样酷炫的动态磁贴效果(示例代码)

    我个人表示非常喜欢微软新一代的产品,先不管它产品的成熟与否,但是它带来的是全新的产品.所谓全新,是指在用户体验上,苹果这些年的成功使得所有产品都在模仿它的界面,包括安卓在内,不知道大家的感觉如何,反正我是对这些圆角矩形产生了审美疲劳(苹果以及安卓的粉丝勿喷,这里仅仅是从界面上评价,事实上从整体上来说,微软还是有差距的),当年wp的推出让我眼前一亮,马上喜欢上了Metro风格的产品,直至今天wp8以及win8开始越来越成熟. 写的不好,欢迎各位看官指正批评,不欢迎无故猛喷.大神请绕道. 废话少说,

  • 打造酷炫的AndroidStudio插件

    前面几篇文章学习了AndroidStudio插件的基础后,这篇文章打算开发一个酷炫一点的插件.因为会用到前面的基础,所以如果没有看前面系列文章的话,请先返回.当然,如果有基础的可以忽略之.先看看本文实现的最终效果如下(好吧,很多人说看的眼花): 虽然并没有什么实际用途,但是作为学习插件开发感觉挺有意思的. 1. 基本思路 基本思路可以归结如下几步: 1).通过Editor对象可以拿到封装代码编辑框的JComponent对象,即调用如下函数:JComponent component = edito

  • jq实现酷炫的鼠标经过图片翻滚效果

    短短的十多行代码就实现了一个酷炫的图片翻滚代码,要实现这个效果并不难,只要思路对了,一切都好办,不多说了,直接上代码看效果! html结构: 复制代码 代码如下: <ul class="list"> <li><img src="images/10.jpg" alt="" /><a href="#"><span>1</span></a></

  • 一个酷炫的Android图表制作框架

    一.概述 最近项目中需要制作柱形图以及折线图,所以便在网上搜索了一下这方面的开源框架,最后找到了这个酷炫的框架,不仅支持各种各样的图形制作,包括折线图.柱形图.饼状图等,而且提供了丰富的API接口,等着你去自定义,只要花点心思便能 DIY 出你心仪的图表类型,使用起来也是相当的简单. 从效果图可以看到,这个框架是相当酷炫的啊,在这里附上该框架的github地址hellocharts-android,有兴趣的不妨去 star 一下 二.炫酷的柱形图 可以看到柱形图也是能玩出花样来的,绚丽的色彩,自

  • 基于 Vue 实现一个酷炫的 menu插件

    写在前面 最近看到一个非常酷炫的menu插件,一直想把它鼓捣成vue形式,谁让我是vue的死灰粉呢,如果这都不算爱:pensive:.:laughing:开个小玩耍,我们一起来探索黑魔法吧.观看本教程的读者需要具备一定的vue和css3的知识. 本文结构 1.效果演示 2.使用方法介绍 3.关键步骤讲解 正文 1.效果演示 pic_1 pic2 pic_3 在线演示 live demo 2.使用介绍 项目地址:github.com/MingSeng-W/vue-bloom-menu ,clone

随机推荐