SwiftUI 学习笔记 45:项目 9-3 绘画
今天,我们将通过查看特殊效果和动画来将你的绘画技巧发挥到极致。正当我们处于绘画的前沿时,可以公平地说,这些技能不太可能在日常编码中使用,但是正如Ralph Waldo Emerson曾经说过的那样,“我们的目标是超越目标以达到目标。”
当你处理今天的主题时,你将学习如何对形状进行动画处理,这是SwiftUI感觉像魔术一样的另一个实例。但是,正如你之前所看到的,这确实不是魔术– SwiftUI只是响应我们配置视图的方式。这有点像一台Rube Goldberg机器:我们将所有设备正确设置好,使整个机器运转,然后观察正确的输出。
控制动画没有什么不同:我们不想让body视图的属性每秒重新调用60或120次才能获得平滑的动画,因此我们只是提供有关动画进行时应更改内容的说明。它不是很难发现的-也就是说,你不能偶然发现该解决方案-但我希望你同意使用起来很简单。
SwiftUI中的特殊效果:模糊,融合等
SwiftUI使我们能够出色地控制视图的呈现方式,包括应用实时模糊,混合模式,饱和度调整等功能。
混合模式使我们可以控制一个视图在另一个视图上的渲染方式。默认模式是.normal,它只是将新视图中的像素绘制到后面的任何东西上,但是有很多选项可以控制颜色和不透明度。
举例来说,我们可以在中绘制图像ZStack,然后在顶部添加一个红色矩形,该矩形使用乘以混合模式绘制:
1 | ZStack { |
之所以称为“乘”,是因为它将每个源像素颜色与目标像素颜色相乘-在我们的示例中,是图像的每个像素和矩形的每个像素在顶部。每个像素具有RGBA的颜色值,范围从0(没有该颜色)到1(所有颜色),因此所得的最高颜色为1x1,最低的颜色为0x0。
对纯色使用乘法会产生一种非常常见的色调效果:黑色保持黑色(因为它们的颜色值为0,所以无论你将顶部乘以0都将产生0),而较浅的颜色会变成各种阴影着色。
实际上,乘法是如此普遍,以至于有一个快捷键修饰符,这意味着我们可以避免使用ZStack:
1 | var body: some View { |
还有很多其他混合模式可供选择,值得花一些时间尝试一下它们的工作方式。另一个流行的效果称为screen,它与乘法相反:将颜色反转,执行乘法,然后再次反转,从而得到更亮的图像而不是更暗的图像。
例如,我们可以在内的各个位置绘制三个圆Stack,然后使用滑块控制其大小和重叠:
1 | struct ContentView: View { |
如果你特别观察,你可能会注意到中间完全混合的颜色不是很白,而是一种非常淡的淡紫色。这样做的原因是Color.red,Color.green和Color.blue不完全的颜色; 使用时没有看到纯红色Color.red。相反,你会看到SwiftUI的自适应颜色被设计为在黑暗模式和明亮模式下都看起来不错,因此它们是红色,绿色和蓝色的自定义混合色,而不是纯色。
如果你想看到混合红色,绿色和蓝色的全部效果,则应使用以下三种自定义颜色:
1 | .fill(Color(red: 1, green: 0, blue: 0)) |
我们还可以应用许多其他实时效果,并且我们已经blur()在项目3中进行了回顾。因此,在继续进行操作之前,我们再来看一个:saturation(),它可以调整视图中使用的颜色数量。给它一个介于0(无颜色,只有灰度)和1(全色)之间的值。
我们可以写一些代码来证明两者blur()并saturation()在同一视图,就像这样:
1 | Image("PaulHudson") |
使用该代码,将滑块设为0意味着图像模糊无色,但是当你将滑块向右移动时,它将获得色彩并变得清晰-所有这些都以闪电般的速度呈现。
使用animatableData对简单形状进行动画处理
现在,我们已经涵盖了与绘图有关的各种任务,在项目6中,我们研究了动画,因此现在我想将这两件事放在一起。
首先,让我们构建一个可以用作示例的自定义形状-这是梯形形状的代码,梯形形状是具有直边且一对相对边平行的四边形:
1 | struct Trapezoid: Shape { |
现在,我们可以在视图内部使用该视图,并为其插入量传递一些本地状态,以便我们可以在运行时修改该值:
1 | struct ContentView: View { |
每次点击梯形时,insetAmount都会将其设置为新值,从而导致形状被重绘。
如果我们可以对插图的变化进行动画处理,那不是很好吗?当然可以–尝试将onTapGesture()闭包更改为此:
1 | .onTapGesture { |
现在再次运行它,……没有任何改变。我们已经要求动画,但是我们没有动画–有什么用?
以前查看动画时,我要求你print()在body属性内添加一个调用,然后这样说:
”你应该看到的是它会打印出2.0、3.0、4.0等。同时,按钮可以顺畅地向上或向下缩放-不仅可以直接跳至缩放2、3和4。这里实际发生的是,SwiftUI在绑定更改之前正在检查视图的状态,绑定更改后,我们视图的目标状态发生了变化,然后应用动画从A点到达B点。”
因此,一旦将self.insetAmount其设置为新的随机值,它将立即跳至该值并将其直接传递给它Trapezoid-动画发生时它不会传递很多中间值。这就是为什么我们的梯形从插图跳到插图。它甚至不知道动画正在发生。
我们只能用四行代码来解决这个问题,其中只有一行是大括号。但是,即使这段代码很简单,它的工作方式也可能使你的大脑弯曲。
首先,代码– Trapezoid现在将此新的计算属性添加到结构中:
1 | var animatableData: CGFloat { |
现在,你可以再次运行该应用程序,并通过平滑的动画查看梯形形状的变化。
这里发生的事情非常复杂:当我们使用时withAnimation(),SwiftUI会立即将state属性更改为新值,但在幕后,随着动画的进行,它还会跟踪随时间变化的值。随着动画的进行,SwiftUI会将animatableData形状的属性设置为最新值,这取决于我们决定什么意思–在本例中,我们直接将其分配给insetAmount,因为这就是我们要进行动画处理的东西。
记住,SwiftUI在应用动画之前先评估视图状态,然后再应用动画。可以看到我们最初有计算结果为的代码Trapezoid(insetAmount: 50),但是在选择了一个随机数之后,我们最终得到了(例如)Trapezoid(insetAmount: 62)。因此,它将在动画的整个长度内插值50到62,每次将animatableData形状的属性设置为最新的插值值– 51、52、53,依此类推,直到达到62。
使用AnimatablePair对复杂形状进行动画处理
SwiftUI使用animatableData属性让我们对形状的变化进行动画处理,但是当我们希望对两个,三个,四个或更多个属性进行动画处理时会发生什么呢?animatableData是一个属性,这意味着它必须始终是一个值,但是我们要决定它是什么类型的值:它可以是单个值CGFloat,也可以是包含在称为的特殊包装器中的两个值AnimatablePair。
为了尝试这一点,让我们看一下一个名为的新形状Checkerboard,该形状必须以一定数量的行和列创建:
1 | struct Checkerboard: Shape { |
现在,我们可以使用一些状态属性在SwiftUI视图中创建一个4x4棋盘格,我们可以使用点击手势来更改这些属性:
1 | struct ContentView: View { |
运行该命令后,你应该可以点击黑色正方形,以查看棋盘格从4x4跳到8x16的情况,即使更改位于一个withAnimation()块内,也没有动画。
与更简单的形状一样,此处的解决方案是实现一个animatableData属性,该属性将随着动画的进行而设置为中间值。不过,这里有两个问题:
我们有两个要设置动画的属性,而不是一个。
我们的row和column属性是整数,SwiftUI不能插入整数。
为了解决第一个问题,我们将使用一种名为的新类型AnimatablePair。顾名思义,它包含一对可动画设置的值,并且由于两个值都可以设置动画,因此AnimatablePair可以设置动画。我们可以使用.first和读取配对值.second。
要解决的第二个我们只是做一些类型转换的问题:我们可以转换Double到Int仅通过使用Int(someDouble),并通过走另一条路Double(someInt)。
因此,要使我们的棋盘动画化行数和列数的变化,请添加以下属性:
1 | public var animatableData: AnimatablePair<Double, Double> { |
现在,当你运行该应用程序时,你应该发现更改进行得很顺利–或由于我们将数字四舍五入为整数而达到了你期望的平滑。
当然,下一个问题是:我们如何为三个属性设置动画?还是四个?
为了回答这个问题,让我向你展示animatableDataSwiftUI EdgeInsets类型的属性:
AnimatablePair<CGFloat, AnimatablePair<CGFloat, AnimatablePair<CGFloat, CGFloat>>>
是的,它们使用三个独立的可动画对,然后使用诸如的代码对它们进行挖掘newValue.second.second.first。
我不会断言这是最优雅的解决方案,但我希望你能理解它存在的原因:因为SwiftUI可以读取和写入形状的可动画显示的数据,而无论该数据是什么或意味着什么,它都不会body在动画制作过程中,无需每秒60甚至120次重新调用视图的属性–只需更改实际更改的部分即可。
使用SwiftUI创建呼吸描记器
为了完成一些真正涉及绘图的工作,我将引导你完成使用SwiftUI创建一个简单的描记器的过程。“旋转描记器”是玩具的商标名称,你可以将铅笔放在一个圆圈内,然后将其绕另一个圆圈旋转,从而创建各种被称为轮盘赌的几何图案,例如赌场游戏。
该代码包含一个非常具体的公式。我将对其进行解释,但是如果你不感兴趣,则可以跳过本章,这完全是可以的-这只是为了好玩,这里没有介绍新的Swift或SwiftUI。
我们的算法有四个输入:
内圆的半径。
外圆的半径。
虚拟笔距外圆中心的距离。
要画多少轮盘赌。这是可选的,但我认为它确实有助于显示算法工作时发生的情况。
因此,让我们开始:
1 | struct Spirograph: Shape { |
然后,我们从该数据中准备三个值,从内半径和外半径的最大公约数(GCD)开始。通常使用Euclid算法来计算两个数字的GCD,该算法以稍微简化的形式看起来像这样:
1 | func gcd(_ a: Int, _ b: Int) -> Int { |
请将该方法添加到Spirograph结构中。
其他两个值是内半径和外半径之间的差,以及我们需要执行多少步来绘制轮盘赌-这是360度乘以外半径除以最大公因数再乘以我们输入的金额。当以整数形式提供时,我们所有的输入效果最佳,但是在绘制轮盘时,我们需要使用CGFloat,因此我们还将创建CGFloat输入的副本。
现在将此path(in:)方法添加到Spirographstruct中:
1 | func path(in rect: CGRect) -> Path { |
最后,我们可以通过从0循环到终点,然后将点放置在精确的X / Y坐标上来绘制轮盘赌本身。计算该循环中给定点(称为“θ”)的X / Y坐标是真正的数学知识的来源,但是老实说,我只是将标准方程式从Wikipedia转换为Swift,这不是我梦dream以求的事情!
X等于半径差乘以theta的余弦,X等于距离乘以半径差的余弦除以外半径再乘以theta。
Y等于半径差乘以theta的正弦,即距离减去半径乘以半径差的正弦除以外半径再乘以theta所得的值。
这是核心算法,但是我们将做两个小更改:我们将分别将绘图矩形的宽度或高度的一半加到X和Y上,以使其在绘图空间中居中,并且theta为0 –即,如果这是轮盘中绘制的第一点–我们将打电话move(to:)而不是addLine(to:)路径。
这是该path(in:)方法的最终代码–将此// more code to come注释替换为:
1 | var path = Path() |
我意识到这是一门繁重的数学工作,但即将实现的结果是:我们现在可以在视图中使用该形状,添加各种滑块以控制内半径,外半径,距离,数量甚至颜色:
1 | struct ContentView: View { |
那是很多代码,但是我希望你花些时间运行该应用程序,并欣赏轮盘的精美程度。你所看到的实际上只是轮盘赌的一种形式,被称为下摆线–对算法的细微调整,你可以生成上摆线,甚至更多,以不同的方式精美。
在结束之前,我想提醒你,这里使用的参数方程式是数学标准,而不是我刚发明的东西–我从字面上转到维基百科有关下胚轴的页面(https://en.wikipedia.org/wiki/Hypotrochoid)并将其转换为Swift。
参考资料
- 感谢你赐予我前进的力量