本文通过DesignCode学习并复现代码。在此作记录便于后续学习
Apple Music中通过专辑取重点色后伴随着音乐而随机变换的背景不得不说很是赏心悦目,但是其后复杂的动画效果却让人一时间无法立即看出它是通过哪种规律去进行变换,在html中,我尝试过用css绘制几个从圆心到周边渐隐的圆,然后为其添加一些周期性的运动,使得它能够在动画结束后能正常的回到原点,但是最后只能说不尽人意,没有AM那种灵动的感觉
而在SwiftUI中,可以利用Cavas通过贝塞尔曲线连接为一个图形,再对每个节点进行点的调整 ,在Figma中,我们可以绘制出一个由曲线组成的封闭形状,然后将位图转为SwiftUI中适用的Canvas图形
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.size.width
let height = rect.size.height
path.move(to: CGPoint (x: 0.9923*width, y: 0.42593*height))
path.addCurve(to: CGPoint(x: 0.6355*width, y: height), control1:
CGPoint (x: 0.92554*width, y: 0.77749*height), control2:
CGPoint (x: 0.91864*width, y: height))
path.addCurve(to: CGPoint (x: 0.08995*width, y: 0.60171*height),
control1: CGPoint(x: 0.35237*width, y: height), control2:
CGPoint (x: 0.2695*width, y: 0.77304*height))
path.addCurve(to: CGPoint (x: 0.34086*width, y: 0.06324*height),
control1: CGPoint(x:-0.0896*width,
y: 0.43038*height), control2:
CGPoint (x: 0.00248*width, y: 0.23012*height))
path.addCurve(to: CGPoint (x: 0.9923*width, y: 0.42593*height),
control1: CGPoint(x:0.67924*width,y:-0.10364*height),
control2: CGPoint(x: 1.05906*width, y: 0.07436*height))
path.closeSubpath()
return path
}
接下来 创建一个Canvas 设置好大小,做一点装饰,就构造出来了一个简单的闭合图形
Canvas { context, size in
context.fill(path(in: CGRect(x: 0, y: 0, width: size.width, height:
size.height )), with: .linearGradient(Gradient(colors:
[.pink,.blue]), startPoint: CGPoint(x: 0, y: 0), endPoint:
CGPoint(x: 400, y: 400)))
}
.frame(width: 400, height: 414)
那么 如何让图形的边缘可以动起来? 首先我们可以通过修改每个曲线节点CGPoint的相对位置,对应到代码中则是操作CGPoint的x或者y的数值。落实到SwiftUI中,如何保证图形的边缘既能够不断的变换,同时能够保障图形在一次变换周期结束之后不会重置导致动画间断呢?
好在swiftUI提供了由timeLine创建的动画补间方案,只需要将你的操作对象包裹在TimeLineView{}中,其创建的时间补间动画则可以应用到图形中。
TimelineView(.animation) { timeline in
let now = timeline.date.timeIntervalSinceReferenceDate
let angle = Angle.degrees(now.remainder(dividingBy: 3) * 60)
let x = cos(angle.radians)
Canvas { context, size in
context.fill(path(in: CGRect(x: 0, y: 0, width: size.width, height: size.height ), x: x), with: .linearGradient(Gradient(colors: [.pink,.blue]), startPoint: CGPoint(x: 0, y: 0), endPoint: CGPoint(x: 400, y: 400)))
}
.frame(width: 400, height: 414)
}
其中,使用timeline.date.timeIntervalSinceReferenceDate 获得 由参考时间起的时间, 只要你在timelineview中添加一个Text来展示一下值的结构,则可以看到值是一个不断递进的 规范好的时间值,而且发现我们在并没有设置state的状况下,页面在不断的更新,简而言之就是timelineview中包含的值的变化推动了其子组件的刷新。
在知道了这一特性之后,我们就可以借此来改变节点的值,在改变之前,我们要遵循两个原则
1. 图形的动画应当是连贯的 ———— 通过一个以0为初始值的数值,创建一个在限定区间中不断来回变换的数来确保动画连贯。
2. 变换的数值应当符合三角函数的原则
如此,我们来解读上面的代码: 首先 获取当前的时间now,接下来通过now的方法.reminder()来限制值的活动区间,我们设置为360度的1/3 : dividingBy:3, 这意味着数值将在-1.5与1.5之间活动,并使用Angle.degree()将其转为角度。
接下来,只需对每个节点中的某个值乘以该不断变换的角度的弧度的余弦值,就可以实现其不断变化的状态了。
这样就实现了图形的规则变换,可以看到形状如同呼吸般在持续变换着,并且不会也不应该会产生中断。 但是图像只是这样看着还是有点僵硬,我们再添加一个点试试看
TimelineView(.animation) { timeline in
let now = timeline.date.timeIntervalSinceReferenceDate
let angle = Angle.degrees(now.remainder(dividingBy: 3) * 60)
let x = cos(angle.radians)
Canvas { context, size in
context.fill(path(in: CGRect(x: 0, y: 0, width: size.width, height: size.height ), x: x), with: .linearGradient(Gradient(colors: [.pink,.blue]), startPoint: CGPoint(x: 0, y: 0), endPoint: CGPoint(x: 400, y: 400)))
}
.frame(width: 400, height: 414)
}
应用到canvas的某些点中
func path(in rect: CGRect, x: Double,x2: Double) -> Path {
var path = Path()
let width = rect.size.width
let height = rect.size.height
path.move(to: CGPoint (x: 0.9923*width, y: 0.42593*height))
path.addCurve(to: CGPoint(x: 0.6355*width, y: height), control1:
CGPoint (x: 0.92554*width*x2, y: 0.77749*height), control2:
CGPoint (x: 0.91864*width, y: height*x2))
path.addCurve(to: CGPoint (x: 0.08995*width, y: 0.60171*height),
control1: CGPoint(x: 0.35237*width*x, y: height), control2:
CGPoint (x: 0.2695*width, y: 0.77304*height))
path.addCurve(to: CGPoint (x: 0.34086*width, y: 0.06324*height*x),
control1: CGPoint(x:-0.0896*width,
y: 0.43038*height), control2:
CGPoint (x: 0.00248*width, y: 0.23012*height*x))
path.addCurve(to: CGPoint (x: 0.9923*width, y: 0.42593*height),
control1: CGPoint(x:0.67924*width,y:-0.10364*height*x),
control2: CGPoint(x: 1.05906*width, y: 0.07436*height*x2))
path.closeSubpath()
return path
}
多了一组变化值x2的加持,比刚才的图形又灵动了一点。但是离我们的期望还是有一点距离。尝试让图形旋转起来如何?
在struct里添加状态 @State var appear 并赋予初始值false。 接下来在TimelineView 的内容的最后一行添加一个旋转动画.rotationEffect(.degrees(appear ? 360: 30)),标志着该图形开始进行360度的转动。
为TimeLineView创建onAppear属性,当view生成时即执行。
.onAppear{
withAnimation(.linear(duration: 10).repeatForever(autoreverses: true)){
appear = true
}
}
为appear这个toggle添加一个补间动画,并设定它为持续10秒的线性动画 .linear(duration: 10),并且永远持续 .repeatForever .
现在我们再来看这个动画 是不是带劲了许多?
现在把它放在背景里,看下如何。
放上图形动画的代码
//
// BlobView.swift
// DesignCodeForiOS15
//
// Created by vicissitidues on 2022/7/26.
//
import SwiftUI
struct BlobView: View {
@State var appear = false
var body: some View {
TimelineView(.animation) { timeline in
let now = timeline.date.timeIntervalSinceReferenceDate
let angle = Angle.degrees(now.remainder(dividingBy: 3) * 60)
let x = cos(angle.radians)
let angle2 = Angle.degrees(now.remainder(dividingBy: 6) * 10)
let x2 = cos(angle2.radians)
// Text("Value: \(x)")
Canvas { context, size in
context.fill(path(in: CGRect(x: 0, y: 0, width: size.width, height: size.height ), x: x, x2: x2), with: .linearGradient(Gradient(colors: [.pink,.blue]), startPoint: CGPoint(x: 0, y: 0), endPoint: CGPoint(x: 400, y: 400)))
}
.frame(width: 400, height: 414)
.rotationEffect(.degrees(appear ? 360: 30))
}
.onAppear{
withAnimation(.linear(duration: 10).repeatForever(autoreverses: true)){
appear = true
}
}
}
func path(in rect: CGRect, x: Double,x2: Double) -> Path {
var path = Path()
let width = rect.size.width
let height = rect.size.height
path.move(to: CGPoint (x: 0.9923*width, y: 0.42593*height))
path.addCurve(to: CGPoint(x: 0.6355*width, y: height), control1:
CGPoint (x: 0.92554*width*x2, y: 0.77749*height), control2:
CGPoint (x: 0.91864*width, y: height*x2))
path.addCurve(to: CGPoint (x: 0.08995*width, y: 0.60171*height),
control1: CGPoint(x: 0.35237*width*x, y: height), control2:
CGPoint (x: 0.2695*width, y: 0.77304*height))
path.addCurve(to: CGPoint (x: 0.34086*width, y: 0.06324*height*x),
control1: CGPoint(x:-0.0896*width,
y: 0.43038*height), control2:
CGPoint (x: 0.00248*width, y: 0.23012*height*x))
path.addCurve(to: CGPoint (x: 0.9923*width, y: 0.42593*height),
control1: CGPoint(x:0.67924*width,y:-0.10364*height*x),
control2: CGPoint(x: 1.05906*width, y: 0.07436*height*x2))
path.closeSubpath()
return path
}
}
struct BlobView_Previews: PreviewProvider {
static var previews: some View {
BlobView()
}
}
//struct BlobShape: Shape {
// }
总结:
Canvas 创建一个有贝塞尔曲线构成的封闭图形。
TimeLineView创建一个根据提供的Schedule进行更新的视图。
rotationEffect 对图形进行自定义的转动操作。