SwiftUI学习100天(Day45 - 项目 9,第三部分)

原创链接:https://www.hackingwithswift.com/100/swiftui
以下内容仅供学习参考:

今天,我们将通过观察特效和动画,将你的绘画技巧发挥到极致。由于我们正处于绘图的前沿,可以公平地说这些技能不太可能用于日常编码,但正如拉尔夫沃尔多爱默生曾经说过的那样,“我们的目标是超越目标,达到目标。”
在学习今天的主题时,你将学习如何为形状设置动画,这是 SwiftUI 感觉有点像魔术的另一个实例。不过,正如你之前所见,它真的不是魔法——SwiftUI 只是响应我们配置视图的方式。这有点像 Rube Goldberg 机器:我们把事情设置得完全正确,让整个机器运转起来,然后观察正确的输出结果。
控制动画也不例外:我们不希望body
每秒重新调用视图的属性 60 或 120 次以获得流畅的动画,因此我们只是提供说明随着动画的进行应该改变什么。它不是很容易被发现——也就是说,你不可能偶然发现这个解决方案——但我希望你会同意它使用起来很简单。
今天你有三个主题要完成,如果你有勇气的话,再加一个额外的主题。你将了解混合模式、animatableData
、AnimatablePair
等。

SwiftUI 中的特殊效果:模糊、混合等
SwiftUI 使我们能够非凡地控制视图的渲染方式,包括应用实时模糊、混合模式、饱和度调整等的能力。
混合模式允许我们控制一个视图在另一个视图之上的渲染方式。默认模式是.normal
,它只是将新视图中的像素绘制到后面的任何内容上,但是有很多选项可以控制颜色和不透明度。
例如,我们可以在ZStack
中绘制一个图像
,然后在顶部添加一个红色矩形,该矩形是使用乘法混合模式绘制的:
“乘法”之所以如此命名,是因为它将每个源像素颜色与目标像素颜色相乘——在我们的例子中,图像的每个像素和顶部矩形的每个像素。每个像素都有 RGBA 的颜色值,范围从 0(没有该颜色)到 1(所有该颜色),因此最高的结果颜色将为 1x1,最低的为 0x0。
对纯色使用 multiply 会应用一种非常常见的色调效果:黑色保持黑色(因为它们的颜色值为 0,所以无论你在上面放什么,乘以 0 都会产生 0),而较浅的颜色会变成各种阴影着色。
事实上,乘法是如此常见以至于有一个快捷修饰符意味着我们可以避免使用ZStack
:
还有许多其他混合模式可供选择,值得花一些时间试验看看它们是如何工作的。另一种流行的效果称为screen,它与 multiply 相反:它反转颜色,执行 multiply,然后再次反转它们,从而产生更亮的图像而不是更暗的图像。
例如,我们可以在ZStack
中的不同位置渲染三个圆
,然后使用滑块来控制它们的大小和重叠:
如果你特别细心,你可能会注意到中间完全混合的颜色不是很白——它是一种非常淡的淡紫色。原因是Color.red
、Color.green
和Color.blue
不完全是那些颜色;使用Color.red
时你看不到纯红色
。相反,你看到的是 SwiftUI 的自适应颜色,旨在在暗模式和亮模式下看起来都不错,因此它们是红色、绿色和蓝色的自定义混合,而不是纯色调。
如果你想看到混合红色、绿色和蓝色的完整效果,你应该使用像这三种自定义颜色:
我们可以应用许多其他实时效果,我们已经在项目 3 中回顾过blur()
。所以,在我们继续之前,让我们再看一个:saturation()
,它调整视图内使用的颜色量。给它一个介于 0(无颜色,只有灰度)和 1(全色)之间的值。
我们可以编写一些代码来在同一视图中演示blur()
和
saturation()
,如下所示
:
使用该代码,将滑块设置为 0 意味着图像模糊且无色,但是当你将滑块向右移动时,它会获得颜色并变得清晰——所有这些都以闪电般的速度呈现。



使用 animatableData 为简单的形状制作动画
我们现在已经涵盖了各种与绘图相关的任务,并且在项目 6 中我们研究了动画,所以现在我想看看将这两件事放在一起。
首先,让我们构建一个可以用作示例的自定义形状——这里是梯形的代码,它是一个四边形,有直边,其中一对对边平行:
我们现在可以在视图中使用它,为其插入量传入一些本地状态,以便我们可以在运行时修改值:
每次点击梯形时,insetAmount
都会设置为一个新值,导致重新绘制形状。
如果我们可以为插图中的变化设置动画不是很好吗?当然可以——尝试将onTapGesture()
闭包更改为:
现在再次运行它,并且……什么都没有改变。我们已经要求动画,但我们没有得到动画——这是怎么回事?
之前看动画的时候,让你在body
属性里面添加一个调用
print()
,然后是这样说的:
”你应该看到它打印出 2.0、3.0、4.0 等等。同时,按钮平滑地向上或向下缩放——它不只是直接跳到缩放 2、3 和 4。这里实际发生的是 SwiftUI 在绑定更改之前检查我们视图的状态,检查绑定更改后视图的目标状态,然后应用动画从 A 点到达 B 点。”
因此,一旦insetAmount
设置为新的随机值,它会立即跳转到该值并将其直接传递给Trapezoid
- 它不会在动画发生时传递大量中间值。这就是为什么我们的梯形从一个插图跳到另一个插图;它甚至不知道动画正在发生。
我们只需四行代码就可以解决这个问题,其中一行只是一个右括号。然而,尽管这段代码很简单,但它的工作方式可能会让你费尽心思。
首先,代码——现在将这个新的计算属性添加到Trapezoid
结构中:
你现在可以再次运行该应用程序并看到我们的梯形以流畅的动画改变形状。
这里发生的事情非常复杂:当我们使用withAnimation()
时
,SwiftUI 会立即将我们的状态属性更改为其新值,但在后台它还会跟踪随时间变化的值作为动画的一部分。随着动画的进行,SwiftUI 会将animatableData
我们形状的属性设置为最新值,由我们来决定这意味着什么——在我们的例子中,我们直接将它分配给insetAmount
,因为这是我们想要动画的东西。
请记住,SwiftUI 在应用动画之前评估我们的视图状态,然后再次评估。它可以看到我们最初的代码评估为Trapezoid(insetAmount: 50)
,但是在选择了一个随机数之后我们最终得到了(例如)Trapezoid(insetAmount: 62)
。因此,它将在我们的动画长度内插值 50 到 62 之间,每次都将animatableData
我们形状的属性设置为最新的插值值——51、52、53 等等,直到达到 62。



使用 AnimatablePair 为复杂形状制作动画
SwiftUI 使用一个animatableData
属性让我们对形状的变化进行动画处理,但是当我们想要两个、三个、四个或更多属性进行动画处理时会发生什么?animatableData
是一个属性,这意味着它必须始终是一个值,但是我们可以决定它是什么类型的值:它可能是单个Double
,也可能是包含在名为 的特殊包装器中的两个值AnimatablePair
。
为了尝试这一点,让我们看看一个名为 Checkerboard
的新形状
,它必须使用一定数量的行和列来创建:
我们现在可以在 SwiftUI 视图中创建一个 4x4 棋盘,使用一些我们可以使用点击手势更改的状态属性:
当它运行时,你应该能够点击黑色方块以查看棋盘从 4x4 跳到 8x16,没有动画,即使变化是在一个withAnimation()
块内。
与更简单的形状一样,这里的解决方案是实现一个animatableData
属性,该属性将随着动画的进行而设置为中间值。不过,这里有两个问题:
我们有两个要设置动画的属性,而不是一个。
我们的
row
和column
属性是整数,SwiftUI 不能插入整数。
为了解决第一个问题,我们将使用一种名为AnimatablePair
. 顾名思义,它包含一对可设置动画的值,并且因为它的两个值都可以设置动画,AnimatablePair
所以它本身也可以设置动画。我们可以使用.first
和.second
从对中读取单个值
。
为了解决第二个问题,我们只需要进行一些类型转换:我们可以使用 .Int(someDouble)
将 一个
Double
使用
,而反过来转换
为一个Int
Double(someInt)。
因此,要使我们的棋盘动画显示行数和列数的变化,请添加此属性:
现在,当你运行该应用程序时,你应该会发现变化很顺利地发生了——或者正如你所期望的那样顺利,因为我们将数字四舍五入为整数。
当然,下一个问题是:我们如何为三个属性设置动画?还是四个?
为了回答这个问题,让我向你展示SwiftUI的EdgeInsets
类型的animatableData
属性:
是的,他们使用三个独立的动画对,然后使用诸如newValue.second.second.first
.
我不会声称这是最优雅的解决方案,但我希望你能理解它存在的原因:因为 SwiftUI 可以读取和写入形状的可动画数据,而不管数据是什么或它意味着什么,它不会body
在动画期间,每秒需要 60 次甚至 120 次重新调用我们视图的属性——它只会更改实际正在更改的部分。



使用 SwiftUI 创建呼吸描记器
为了完成真正通过绘图进入城镇的东西,我将引导你使用 SwiftUI 创建一个简单的螺旋仪。“Spirograph”是一种玩具的商标名称,你可以将一支铅笔放在一个圆圈内,然后绕着另一个圆圈的圆周旋转,从而创造出各种几何图案,这些图案被称为轮盘赌——就像赌场游戏一样。
此代码涉及一个非常具体的方程式。我将对其进行解释,但如果你不感兴趣,完全可以跳过本章——这只是为了好玩,这里没有介绍新的 Swift 或 SwiftUI。
我们的算法有四个输入:
内圆的半径。
外圆的半径。
虚拟笔距外圈圆心的距离。
抽取多少轮盘赌。这是可选的,但我认为它确实有助于显示算法工作时发生的情况。
那么,让我们从这个开始:
然后我们从该数据准备三个值,从内半径和外半径的最大公约数 (GCD) 开始。计算两个数的 GCD 通常使用 Euclid 算法完成,其稍微简化的形式如下所示:
请将该方法添加到Spirograph
结构中。
其他两个值是内半径和外半径之间的差值,以及绘制轮盘需要执行多少步——这是 360 度乘以外半径除以最大公约数,再乘以我们输入的数量。我们所有的输入在以整数形式提供时效果最佳,但在绘制轮盘赌时我们需要使用Double
,因此我们还将创建Double
输入的副本。
现在将此path(in:)
方法添加到Spirograph
结构中:
最后,我们可以通过从 0 到终点循环并将点放置在精确的 X/Y 坐标处来绘制轮盘赌本身。计算该循环中给定点的 X/Y 坐标(称为“theta”)是真正的数学用武之地,但老实说,我只是将标准方程式从维基百科转换为 Swift——这不是我梦想记住的东西!
X等于半径差乘以theta的余弦,加上距离乘以半径差的余弦除以外半径乘以theta。
Y等于半径差乘以theta的正弦,减去距离乘以半径差的正弦除以外半径乘以theta。
这是核心算法,但我们要做两个小改动:我们将分别向 X 和 Y 添加绘图矩形宽度或高度的一半,以便它在我们的绘图空间中居中,并且如果 theta 为 0 – 也就是说,如果这是我们轮盘赌中被抽取的第一个点 – 我们的为路径调用move(to:)
而不是addLine(to:)
。
这是该方法的最终代码path(in:)
——将// more code to come
注释替换为:
我意识到这是大量繁重的数学运算,但回报即将到来:我们现在可以在视图中使用该形状,添加各种滑块来控制内半径、外半径、距离、数量,甚至颜色:
那是很多代码,但我希望你花时间运行该应用程序并欣赏轮盘赌的美妙之处。你所看到的实际上只是轮盘赌的一种形式,称为次摆线——通过对算法进行小幅调整,你可以生成外摆线等等,它们以不同的方式呈现出美丽。
在我结束之前,我想提醒你这里使用的参数方程是数学标准,而不是我刚刚发明的东西——我真的去了维基百科关于 hypotrochoids 的页面 ( https://en.wikipedia.org/wiki/Hypotrochoid )并将它们转换为 Swift。


