SwiftUI 学习笔记 44:项目 9-2 绘画
今天,我们将通过增加一些创意来继续研究SwiftUI的绘图系统-我想你会惊讶地发现,仅通过将大多数已知知识与几种新技术结合起来,就可以使一件有趣的事情变得如此容易。
今天,你还将遇到drawingGroup()修饰符,该修饰符使我们可以将视图渲染结合到一起,并由Apple的高性能图形API Metal提供支持。很多人都问我的过去,我是否打算写一本关于金属,答案是明确的不 -不仅是有一个非常好的一个已经,但它也是非常难以得到什么好东西了的API 。
那不是因为Metal不好-相信我,这太不可思议了!–而是因为苹果最好的工程师在与Metal一起工作时尽了最大的努力使SwiftUI尽可能高效,而且直率地说,我不可能做得更好。
如你所见,切换Metal并不容易,即使很容易做到。著名软件开发人员肯特·贝克(Kent Beck)曾经说过,我们的流程应该是“使其正常运行,使其正确,快速”。但是,如果你发现绘图工作迅速而无需切换到Metal,通常最好保持原样。
无论如何,足够的聊天–我说过我们会做点有趣的事情,让我们开始吧!
今天,你有三个课题合作,通过在其中你将了解CGAffineTransform,ImagePaint,drawingGroup(),等等。
使用CGAffineTransform和奇数填充来变换形状
当你超越简单的形状和路径时,SwiftUI的两个有用功能会合在一起,以极少的工作量创建出漂亮的效果。第一个是CGAffineTransform,它描述了如何旋转,缩放或剪切路径或视图。第二个是奇数填充,它使我们可以控制应如何渲染重叠的形状。
为了演示这两种方法,我们将用几个旋转的椭圆形花瓣创建一个花朵形状,每个椭圆形围绕一个圆放置。这背后的数学方法相对简单,只有一个要点:CGAffineTransform以弧度而非角度来测量角度。如果自从上学以来已经有一段时间了,那么你至少需要知道的是:3.141弧度等于180度,因此3.141弧度乘以2等于360度。3.141并非巧合:实际值是数学常数pi。
因此,我们要做的如下:
创建一个新的空路径。
从0到pi乘以2(弧度为360度),然后每次计数为pi的八分之一,这将为我们提供16个花瓣。
创建一个等于当前数字的旋转变换。
加上此旋转,移动量等于绘制空间宽度和高度的一半,因此每个花瓣都以我们的形状为中心。
为花瓣创建一个新路径,该路径等于特定大小的椭圆。
将我们的变换应用到该椭圆,以便将其移到位置。
将花瓣的路径添加到我们的主路径中。
一旦你看到代码正在运行,这将更有意义,但是首先我想添加三件事:
旋转然后移动的东西不会产生与移动然后旋转的结果相同的结果,因为先旋转时,它的移动方向将与未旋转时的不同。
为了真正帮助你了解发生了什么,我们将使花瓣椭圆使用一些可以从外部传递的属性。
1…5如果你想一次计数一次,则这样的范围非常好,但是如果你想以2s为单位进行计数,或者在我们的案例中以“ pi / 8”为单位进行计数,则应该stride(from:to:by:)改用。
好了,足够多的讨论–现在将此形状添加到你的项目中:
1 | struct Flower: Shape { |
我意识到这是很多代码,但是希望当你尝试一下时,它会变得更加清晰。修改ContentView为:
1 | struct ContentView: View { |
现在尝试一下。一旦开始拖动offset和width滑块,你应该就能确切地看到代码的工作原理–它只是一系列旋转的椭圆,呈圆形排列。
这本身是有趣的,但有一个小的变化,我们可以去从有趣到崇高。如果你查看绘制椭圆的方式,它们经常重叠-有时一个椭圆绘制在另一个椭圆上,有时绘制在其他多个椭圆上。
如果我们使用纯色填充路径,则会得到相当不令人印象深刻的结果。像这样尝试:
1 | Flower(petalOffset: petalOffset, petalWidth: petalWidth) |
但是,作为一种替代方法,我们可以使用奇偶规则填充形状,该规则决定路径的一部分是否应根据其包含的重叠进行着色。它是这样的:
如果路径没有重叠,它将被填充。
如果另一条路径重叠,则重叠的部分将不会被填充。
如果第三个路径与前两个路径重叠,则会被填充。
…等等。
仅实际重叠的部分受此规则影响,并且会产生一些非常漂亮的结果。更好的是,Swift UI使其使用起来很简单,因为每当我们调用fill()形状时,我们都可以传递一个FillStyle结构,该结构要求启用奇偶规则。
尝试一下:
1 | Flower(petalOffset: petalOffset, petalWidth: petalWidth) |
现在运行程序并开始播放-老实说,鉴于我们所做的工作很少,结果非常诱人!
使用ImagePaint创作边框和填充(平铺图案)
SwiftUI严重依赖于协议,使用绘图时可能会有些混乱。例如,我们可以Color用作视图,但它也符合ShapeStyle–用于填充,笔触和边框的不同协议。
实际上,这意味着我们可以修改默认的文本视图,使其具有红色背景:
1 | Text("Hello World") |
或红色边框:
1 | Text("Hello World") |
相反,我们可以将图像用作背景:
1 | Text("Hello World") |
但是使用相同的图像作为边框将不起作用:
1 | Text("Hello World") |
如果你考虑一下,这是有道理的-除非图像的大小正确,否则你对外观的控制将非常有限。
为了解决这个问题,SwiftUI为我们提供了一种专用类型,该类型可以完全控制应如何渲染图像的方式包装图像,这又意味着我们可以将它们用于边框和填充而不会出现问题。
该类型称为ImagePaint,它是使用一到三个参数创建的。至少你需要给它一个Image作为其第一个参数,但是你也可以在该图像内提供一个矩形,用作在0到1(第二个参数)范围内指定的图形源,以及该图像的比例(第三个参数)。这些第二和第三个参数具有合理的默认值“整个图像”和“ 100%比例”,因此有时你可以忽略它们。
举例来说,我们可以使用0.2的比例尺绘制示例图像,这表示它的显示尺寸为正常尺寸的1/5:
1 | Text("Hello World") |
如果要尝试使用该sourceRect参数,请确保输入CGRect相对大小和位置:0表示“开始”,1表示“结束”。例如,这将显示示例图像的整个宽度,但仅显示中间一半:
1 | Text("Hello World") |
值得补充的是,ImagePaint它可以用于查看背景以及笔触形状。例如,我们可以创建一个胶囊,将示例图像平铺为笔划:
1 | Capsule() |
ImagePaint 会自动将其图像平铺,直到其填满为止-它可以与背景,笔触,边框和任何大小的填充一起使用。
使用drawingGroup()启用高性能金属渲染
SwiftUI默认使用Core Animation进行渲染,提供了出色的开箱即用性能。但是,对于复杂的渲染,你可能会发现代码开始变慢–每秒60帧(FPS)以下的任何问题都是一个问题,但实际上你应该将目标更高,因为许多iOS设备现在以120fps的速度渲染。
为了说明这一点,让我们看一些示例代码。我们将创建一个颜色循环视图,以各种颜色呈现同心圆。结果看起来像是一个径向渐变,但是我们将添加两个属性以使其更具可定制性:一个用于控制应该绘制多少个圆形,另一个用于控制颜色循环–它将能够移动渐变周围的开始和结束颜色。
我们可以通过使用Color(hue:saturation:brightness:)初始化程序来获得颜色循环效果:色相是从0到1的一个值,控制着我们看到的颜色的种类–红色是0到1,介于其间的所有其他色相。为了弄清楚特定圆圈的色相,我们可以取圆圈数(例如25),除以有多少圆圈(例如100),然后加上颜色循环量(例如0.5)。因此,如果我们是100的第25圈,循环量为0.5,则我们的色相将为0.75。
这里的一个小复杂性是,色相在达到1.0后不会自动换行,这意味着1.0的色相等于0.0的色相,但是1.2的色相不等于0.2的色相。结果,我们将手工包裹色相:如果色相超过1.0,我们将减去1.0,以确保它始终位于0.0到1.0的范围内。
这是代码:
1 | struct ColorCyclingCircle: View { |
现在,我们可以在布局中使用它,将其颜色循环绑定到由滑块控制的局部属性:
1 | struct ContentView: View { |
如果你运行该应用程序,你会看到我们有一个整齐的色波效果,完全可以通过在滑块上拖动来控制,并且效果非常流畅。
你现在所看到的是由Core Animation驱动的,这意味着它将把我们的100个圆变成在屏幕上绘制的100个独立视图。这在计算上是昂贵的,但是如你所见,它运行良好-我们获得了平稳的性能。
但是,如果稍微增加复杂度,我们会发现事情并不是那么乐观。strokeBorder()用这个替换现有的修饰符:
1 | .strokeBorder(LinearGradient(gradient: Gradient(colors: [ |
现在,这将呈现一个柔和的渐变,在圆的顶部显示明亮的颜色,在底部显示较暗的颜色。而现在,当你运行应用程序,你会发现它运行慢得多- SwiftUI正在努力使100个梯度为100点独立的观点的一部分。
我们可以通过应用一个称为的新修饰符来解决此问题drawingGroup()。这告诉SwiftUI,在将视图内容作为单个呈现的输出放回到屏幕上之前,应将视图的内容呈现到屏幕外的图像中,这要快得多。在幕后,此功能由Metal提供支持,Metal是Apple的框架,可直接与GPU协同工作以实现极快的图形。
因此,将ColorCyclingCircle主体修改为此:
1 | var body: some View { |
现在,再次运行它-仅需添加一点点,就可以正确渲染所有内容,即使使用渐变色也可以全速返回。
重要提示:该drawingGroup()修改有助于了解并保持你的军火库,以此来解决性能问题,当你打他们,但你应该不会使用它,往往。添加屏幕外渲染过程可能会降低SwiftUI进行简单绘图的速度,因此,在尝试引入之前,应等待直到遇到实际的性能问题drawingGroup()。
参考资料
- 感谢你赐予我前进的力量