Chapter 16 圆环菜单
本章的主要目标是完成菜单的两种状态,我们会创建一些结构体,完成两种状态间的转换。例如,我们不再给一条厚线创建另外一个动画,而是创建一个数组,保存我们的目标 frame,在动画过程中更新圆环 frame 的大小。我们会在之后的演示中创建所需的结构体。
1. 圆环
这些线是菜单的骨架部分,我们把这个组件分解成几个部分:创建所有的圆环,设置开始状态,设置结束状态,最后添加动画。
先创建厚线、细线、竖线和分割线。
1.1. 粗圆环
先实现粗圆环,这里有两个:一个是小的,直径 28 pt,另外一个是大的,直径 450 pt。
打开 MenuRings.swift
创建一个可变数组,存储粗圆环的尺寸。
var thickRingFrames : [Rect]!
接着,给圆环创建一个变量:
var thickRing : Circle!
接着,写一个方法来创建粗圆环,设置 targets(主体)的内外部的形状,接着把它们的 frames 添加到数组里:
func createThickRing() {
// 创建两个形状
let inner = Circle(center: canvas.center, radius: 14)
let outer = Circle(center: canvas.center, radius: 225)
// 存储每个位置的 frame
thickRingFrames = [inner.frame,outer.frame]
}
之后我们会使用这个 frame 数组来实现我们的动画。现在嘛,我们先实现圆环的开始状态。把下列代码添加到 createThickRing
函数里最下方:
inner.fillColor = clear
inner.lineWidth = 3
inner.strokeColor = COSMOSblue
inner.interactionEnabled = false
thickRing = inner
canvas.add(thickRing)
1.2. 检查一下
为了查看一下你刚刚添加的代码的实际效果,需要在 WorkSpace 里的 setup()
方法添加下面的代码:
canvas.add(MenuRings().canvas)
App 现在看起来应该是下图这个样子:
如果要看一下圆环处于最外层时的状态,在 MenuRings.swift
文件里把下面代码:
thickRing = inner
换成这样:
thickRing = inner
thickRing.frame = outer.frame
就能看到下图这样的效果了:
在继续下一步之前,撤销刚刚做出的修改。
1.3. 细圆环
创建细圆环的过程也是一样的,不同之处在于有多个细圆环。先创建两个数组:
var thinRings : [Circle]!
var thinRingFrames : [Rect]!
我们给细圆环创建数组,因为需要记录五个细圆环。
设计稿也详细说明了开始状态下的内层圆环的直径是 8 pt,外层圆环的直径是 56 pt、78 pt、98 pt 和 156 pt。
创建一个方法,设置和创建全部的圆环,包括内层和外层的:
func createThinRings() {
thinRings = [Circle]()
thinRings.append(Circle(center: canvas.center, radius: 8))
thinRings.append(Circle(center: canvas.center, radius: 56))
thinRings.append(Circle(center: canvas.center, radius: 78))
thinRings.append(Circle(center: canvas.center, radius: 98))
thinRings.append(Circle(center: canvas.center, radius: 102))
thinRings.append(Circle(center: canvas.center, radius: 156))
}
我们创建了一堆形状并添加到数组里,之后可以引用它们。
接下来写一个循环,遍历所有的,设置每个形状的风格。把下列代码添加到 createThinRings
方法里:
thinRingFrames = [Rect]()
for i in 0..<self.thinRings.count {
let ring = self.thinRings[i]
ring.fillColor = clear
ring.lineWidth = 1
ring.strokeColor = COSMOSblue
ring.interactionEnabled = false
if i > 0 {
ring.opacity = 0.0
}
self.thinRingFrames.append(ring.frame)
}
for ring in thinRings {
canvas.add(ring)
}
循环看起来很简单:遍历所有的圆环,执行一系列设置样式的代码。圆环按照从小到大的顺序添加到数组里,所以数组里的第一个元素成了内层第一个圆环(例如当菜单处于默认状态时)。一开始我们只想看内层圆环,所以把 i > 0 的圆环设置透明度为 0。我们把每个圆环的 frame 尺寸添加到 thinRingFrames
里,最后把所有的圆环添加到 canvas 里。
更新 setup()
方法,如下:
public override func setup() {
createThickRing()
createThinRings()
}
1.4. 检查一下
运行程序,应用看起来如下图所示:
为了看一下外层圆环,在 createThinRings()
方法里,把下列代码:
if i > 0
改成这样:
if i == 0
现在,App 看起来应该是这样的:
撤销刚刚做的修改。
1.5. 破折线
和上面两节做的事情一样,我们要创建一个数组来存储边框为破折线的圆。实际上破折线是最终要填到圆环上,所以我们不需要存储这些破折线。
把下列变量添加到类中:
var dashedRings : [Circle]!
创建三个方法:
func createShortDashedRing(){
}
func createLongDashedRing(){
}
func createDashedRings() {
dashedRings = [Circle]()
createShortDashedRing()
createLongDashedRing()
for ring in self.dashedRings {
ring.strokeColor = COSMOSblue
ring.fillColor = clear
ring.interactionEnabled = false
ring.lineCap = .Butt
self.canvas.add(ring)
}
}
我们需要创建长短两种竖线,粗细程度不同,每种竖线都有自己的模式。
1.6. 短破折线线圆环
短破折线的设置如下:
func createShortDashedRing() {
let shortDashedRing = Circle(center: canvas.center, radius: 82+2)
let pattern = [1.465,1.465,1.465,1.465,1.465,1.465,1.465,1.465*3.0] as [NSNumber]
shortDashedRing.lineDashPattern = pattern
shortDashedRing.strokeEnd = 0.995
let angle = degToRad(-1.5)
let rotation = Transform.makeRotation(angle)
shortDashedRing.transform = rotation
shortDashedRing.lineWidth = 0.0
dashedRings.append(shortDashedRing)
}
这里实际上进行了很多设置,有的设置还依赖于其他的设计因素,所以我在这里将上述代码尽可能地分解:
- 圆圈的直径是
82+2
,我本来可以写84
的,不过+2
实际上值得是lineWidth
的一半。1 pattern
是1.462,....
,使用这个数字实际上是有两个原因的:a)圆圈可以分成 36 个部分,每个部分 10 度,b)1.465*3
表示间隙(例如每个长竖线之间间隔两个空格,外加一个额外的空格,一个三个空格),c)as [NSNumber]
必须要有,因为底层属性需要(例如不是[Double]
)。- 因为模式会从第一条线开始,所以我们需要旋转一点整个图形,这样看起来好像起始位置是空格了,旋转度为
-1.5
度,转换成弧度,创建 transform,应用到形状上。 - 剩下的内容都简单易懂无需解释了。
1.7. 长破折线圆环
longDashedRing
方法如下:
func createLongDashedRing() {
let longDashedRing = Circle(center: canvas.center, radius: 82+2)
longDashedRing.lineWidth = 0.0
let pattern = [1.465,1.465*9.0] as [NSNumber]
longDashedRing.lineDashPattern = pattern
longDashedRing.strokeEnd = 0.995
let angle = degToRad(0.5)
let rotation = Transform.makeRotation(angle)
longDashedRing.transform = rotation
let mask = Circle(center: longDashedRing.bounds.center, radius: 82+4)
mask.fillColor = clear
mask.lineWidth = 8
longDashedRing.layer?.mask = mask.layer
dashedRings.append(longDashedRing)
}
其实和之前的方法很相似,有几个不同之处:
- 模式是
[1.465,1.465*9.0]
,表示每个间隙之后有一个竖线,间隙的宽度比竖线宽 9x。 - 旋转 5 度,将长竖线居中,正好在短竖线间隙的中间。
- 最后一条线有微小的差异,最后一条线稍微可见,所以把
strokeEnd
的值从1
调整成0.995
,隐藏一下。 - 接着,创建遮罩...
更新 setup()
如下:
public override func setup() {
createThickRing()
createThinRings()
createDashedRings()
}
1.8. 检查一下
要看圆环什么样子,需要做一下操作:
把这行代码:
shortDashedRing.lineWidth = 0.0
改成:
shortDashedRing.lineWidth = 4.0
把这行代码:
longDashedRing.lineWidth = 0.0
改成:
longDashedRing.lineWidth = 12.0
运行,效果如下图:
记得把上面的两个变动再改回去。
2. 遮罩
遮罩组件算是个小技巧,在塑造竖线的形状方面,能减轻工作量。默认情况下,竖线是画在中心的外围的,如果你前面已经有了一条 12pt 水平的线,那么上面和下面的线是 6pt。
设计图显示,两个圆圈的初始状态下的直径都是一样的。我们想让竖线看起来像是从基线向外生长...所以,我们给多余的部分盖上遮罩,就看不到多余的部分了。
这也是属性是 12pt 的原因,但是在屏幕上看起来只有 6pt...因为砍掉了 6pt
遮罩是这样工作的:透过遮罩有颜色的地方可以看到下面的对象。所以,我们创建一个 8pt 的实线,它的直径要足够大,这样遮罩的边缘会碰到圆的基线并一直延伸到长竖线的顶部。
如下所示:
欧耶!当你将遮罩应用到某个对象上时,它会基于对象的内部坐标系定位,这就是为什么我们需要用 longDashedRing.bounds.center
来确定遮罩的中心点。
3. 分割线
下一步是创建分割线,将每个图标分隔开来。
首先,创建如下变量:
var menuDividingLines : [Line]!
接着,在类里增加如下方法:
func createMenuDividingLines() {
menuDividingLines = [Line]()
for i in 0...11 {
let line = Line((Point(),Point(54,0)))
line.anchorPoint = Point(-1.88888,0)
line.center = canvas.center
line.transform = Transform.makeRotation(M_PI / 6.0 * Double(i) , axis: Vector(x: 0, y: 0, z: -1))
line.lineCap = .Butt
line.strokeColor = COSMOSblue
line.lineWidth = 1.0
line.strokeEnd = 0.0
canvas.add(line)
menuDividingLines.append(line)
}
}
设置直线的步骤相当简单,我们知道图标内部和外部边缘直接的间隙是 54pt
,那么我们要画的这条分割线也是这么长,设置分割线的风格,改变 anchorPoint
。
4. 锚点
每个可见的对象都有一个锚点,默认位置是在对象视图的居中位置。围绕这个点实现各种变形。例如,如果我只是让一个对象旋转某个角度(正如我在长短竖线那里所做的),那么整个对象都会围绕自己的锚点旋转。2
我想要的效果是,线的角度均匀地分布在两个圆圈之间,依赖于 anchorPoint
属性。我们可以计算每条线的旋转位置 a
和 b
,不过这样创建效果可不太优雅。
我们需要做的是把 anchorPoint
位置偏移,这样我们可以在线的外面围绕一点旋转。唉,还是看图片更容易理解,一图胜千言,效果如下图:
关于锚点的另外一件事情就是,它们的测量和对象视图的空间有关。具体说来,一个视图的中心点是 {0.5,0.5}
,所以,现在我们只需要找出我们需要把锚点放在哪里,这样,54pt
的线就会出现在正确的位置了。
已经知道内圆的半径是 102
(例如,倒数第二个细圆环),我们也知道线的宽度是 54
,所以我们需要做的就是转换相关的坐标:
102/54 = 1.888
由于我们想让点在视图外部距左的距离为 0
,我们需要把值设置成负数,也就是下面这行代码:
line.anchorPoint = CGPointMake(-1.88888,0)
方法中剩下的部分都很简单,把锚点居中,位于 canvas 的中心部分,然后旋转分割线,12 条线都进行同样的操作后,把它们添加到 canvas 和数组里(之后会玩出更多花样的)。
V5。分割线已完成。
哦对了,别忘了,如果我们没有调整分割线的锚点,布局看起来应如下图所示:
setup()
看起来应该是这个样子的:
override func setup() {
self.createThickRing()
self.createThinRings()
self.createDashedRings()
self.createMenuDividingLines()
}
5. 检查一下
如果想看到分割线,在 createMenuDividingLines 里进行修改:
把下面的代码删除:
line.strokeEnd = 0.0
改成:
line.strokeEnd = 1.0
或者注释掉也行。
效果如下图:
如果你想到原来的样子,更改所有之前的变量,看一下圆环外部和直线的状态,效果如下图:
撤销刚刚做的修改,把直线设置成在里面的状态。
V5!
这些线看起来不错。
让我们继续下一章吧。
脚注
1. 我在写的时候就在想,为什么要这样写?不过之后又看了一遍代码之后,我意识到,我有点喜欢这样了,能我记住中心点需要调整一下,虽然 Jake 设计稿里明确直径是 82pt。↩