Chapter 21 完成菜单功能
现在我们要开始完成 Menu.swift
中的代码,计划是:
- 把所有的图层都添加到这个类的画布中
- 给这个类的画布添加一个手势识别器
- 添加一个方法来隐藏或者显示菜单
- 在用户打开或关闭菜单时,播放声音
- 添加一些提示标签来帮助用户使用
1. 添加所有的层
创建下面的类变量:
var menuRings : MenuRings!
var menuIcons : MenuIcons!
var menuSelector : MenuSelector!
var menuShadow : MenuShadow!
var shouldRevert = false
然后,修改 setup()
方法为:
override func setup() {
// 设置透明背景
canvas.backgroundColor = clear
// 让画布尽可能小
canvas.frame = Rect(0,0,80,80)
// 创建圆环
menuRings = MenuRings()
// 创建菜单选择按钮
menuSelector = MenuSelector()
// 创建图标
menuIcons = MenuIcons()
// 创建阴影
menuShadow = MenuShadow()
menuShadow.canvas.center = canvas.bounds.center
// 添加各个部件
canvas.add(menuShadow.canvas)
canvas.add(menuRings.canvas)
canvas.add(menuSelector.canvas)
canvas.add(menuIcons.canvas)
}
然后,在工程的 WorkSpace
中修改 setup()
为:
override func setup() {
canvas.backgroundColor = COSMOSbkgd
let menu = Menu()
menu.canvas.center = canvas.center
canvas.add(menu.canvas)
}
如果现在运行 app,你会看见:
可以看到圆环和图标都显示出来了。
2. 添加手势
现在,回到 MenuSelector.swift
, 剪切整个 createGesture()
并复制到这个类里。
你会看到 Xcode 有一个报错:
要修复很简单,只需要把
self.
改为
self.menuSelector.
最终,你的方法会像这样:
func createGesture() {
canvas.addLongPressGestureRecognizer { (locations, center, state) -> () in
switch state {
case .Changed:
self.menuSelector.update(center)
case .Cancelled, .Ended, .Failed:
self.menuSelector.currentSelection = -1
self.menuSelector.highlight.hidden = true
self.menuSelector.menuLabel.hidden = true
default:
_ = ""
}
}
}
现在,手势识别器已经是菜单层的一部分,而不再属于菜单选择器层。
3. 显示菜单
现在,我们希望在菜单打开或关闭的时候伴随着一系列有序的动画。为了实现这功能,添加以下两个方法到你的类里:
func revealMenu() {
menuShadow.reveal?.animate()
menuRings.thickRingOut?.animate()
menuRings.thinRingsOut?.animate()
menuIcons.signIconsOut?.animate()
delay(0.33) {
self.menuRings.revealHideDividingLines(1.0)
self.menuIcons.revealSignIcons?.animate()
}
delay(0.66) {
self.menuRings.revealDashedRings?.animate()
self.menuSelector.revealInfoButton?.animate()
}
}
func hideMenu() {
menuRings.hideDashedRings?.animate()
menuSelector.hideInfoButton?.animate()
menuRings.revealHideDividingLines(0.0)
delay(0.16) {
self.menuIcons.hideSignIcons?.animate()
}
delay(0.57) {
self.menuRings.thinRingsIn?.animate()
}
delay(0.66) {
self.menuIcons.signIconsIn?.animate()
self.menuRings.thickRingIn?.animate()
self.menuShadow.hide?.animate()
self.canvas.interactionEnabled = true
}
}
3.1. 菜单是否可见
大多数交互都是需要菜单处于可见的状态,所以我们会创建一个检查菜单是否可见的变量,在你的类中添加如下代码:
var menuIsVisible = false
在 revealMenu()
方面的最后添加:
delay(1.0) {
self.menuIsVisible = true
}
我们知道在显示菜单的时候,动画需要花 1 秒钟时间,所以我们设置变量也需要遵循这个逻辑。
在 hideMenu()
方法的头部添加:
self.menuIsVisible = false
这里没有延迟,因为菜单一旦触发隐藏,那菜单就是不可见状态。
你需要把同样的逻辑添加到 revealMenu()
方法中,从而保证状态的正确性。
你的显示和隐藏方法最终会变成这样:
func revealMenu() {
menuIsVisible = false
menuShadow.reveal?.animate()
menuRings.thickRingOut?.animate()
menuRings.thinRingsOut?.animate()
menuIcons.signIconsOut?.animate()
delay(0.33) {
self.menuRings.revealHideDividingLines(1.0)
self.menuIcons.revealSignIcons?.animate()
}
delay(0.66) {
self.menuRings.revealDashedRings?.animate()
self.menuSelector.revealInfoButton?.animate()
}
delay(1.0) {
self.menuIsVisible = true
}
}
func hideMenu() {
self.menuIsVisible = false
menuRings.hideDashedRings?.animate()
menuSelector.hideInfoButton?.animate()
menuRings.revealHideDividingLines(0.0)
delay(0.16) {
self.menuIcons.hideSignIcons?.animate()
}
delay(0.57) {
self.menuRings.thinRingsIn?.animate()
}
delay(0.66) {
self.menuIcons.signIconsIn?.animate()
self.menuRings.thickRingIn?.animate()
self.menuShadow.hide?.animate()
self.canvas.interactionEnabled = true
}
}
4. 行为的逻辑实现
我们首先需要为手势的处理添加一些逻辑。在 switch 语句的 .Cancelled
分支中第一个 if
后面添加如下代码:
if self.menuIsVisible {
self.hideMenu()
} else {
self.shouldRevert = true
}
如果手势结束、被取消或者失败,这个逻辑会检查两件事:如果菜单是可见的则需要反转,否则就设置 shouldRevert
让菜单在打开完毕后自动关闭。
然后,在 revealMenu()
方法中的 delay(1.0)
函数块,添加:
if self.shouldRevert {
self.hideMenu()
self.shouldRevert = false
}
一旦菜单显示完毕,就会检查它是否需要反转。
然后在 switch 语句的开头添加:
case .Began:
self.revealMenu()
当手势开始时(默认按 0.25 秒之后),则开启菜单。
最后,在 hideMenu()
方法的 delay(0.66)
块中添加:
self.canvas.interactionEnabled = true
以及添加如下的代码到手势逻辑中的 .Cancelled
分支:
self.canvas.interactionEnabled = f
这可以有效的防止用户在菜单正在返回关闭状态时对其进行操作。同理,在菜单关闭完成后,需要重新设置它为 true。
你的 createGesture()
方法应该像这样:
func createGesture() {
// 添加长按的手势
canvas.addLongPressGestureRecognizer { (locations, center, state) -> () in
switch state {
case .Began:
self.revealMenu()
case .Changed:
self.menuSelector.update(center)
case .Cancelled, .Ended, .Failed:
self.menuSelector.currentSelection = -1
self.menuSelector.highlight.hidden = true
self.menuSelector.menuLabel.hidden = true
if self.menuIsVisible {
self.hideMenu()
} else {
self.shouldRevert = true
}
self.canvas.interactionEnabled = false
default:
_ = ""
}
}
}
现在,千万要确定你在 Menu
类的 setup 方法中调用了它。
酷!
5. 声音
给菜单添加打开和关闭的声音。添加如下两个类变量:
let hideMenuSound = AudioPlayer("menuClose.mp3")!
let revealMenuSound = AudioPlayer("menuOpen.mp3")!
在 setup()
方法中,调整其参数:
hideMenuSound.volume = 0.64
revealMenuSound.volume = 0.64
然后,在 revealMenu()
方法的顶部,添加:
revealMenuSound.play()
同样,在 hideMenu()
的顶部,也添加:
hideMenuSound.play()
运行一遍听听看,很有趣的声音。
6. 隐藏的提示
让我们添加一个提示标签来告诉用户长按菜单这个操作
我们发现在用户明白如何使用这个 app 之前,需要被提示才能知道去长按菜单。所以我们添加这个标签来帮助他们。我们还需要假设用户在阅读一次提示之后就不再需要看第二次,所以我们还需要防止提示重复出现。
创建如下的类变量:
var instructionLabel : UILabel!
记住我们使用
UILabel
因为我们希望有两行居中显示的文字,TextShape
并不满足这个要求。
添加如下的方法来创建提示:
func createInstructionLabel() {
instructionLabel = UILabel(frame: CGRect(x: 0,y: 0,width: 320, height: 44))
instructionLabel.text = "press and hold to open menu\nthen drag to choose a sign"
instructionLabel.font = UIFont(name: "Menlo-Regular", size: 13)
instructionLabel.textAlignment = .Center
instructionLabel.textColor = .whiteColor()
instructionLabel.userInteractionEnabled = false
instructionLabel.center = CGPointMake(view.center.x,view.center.y - 128)
instructionLabel.numberOfLines = 2
instructionLabel.alpha = 0.0
canvas.add(instructionLabel)
}
然后创建一个计时器来控制标签的显示:
var timer : Timer!
然后,创建一下两个方法来显示和隐藏提示:
func showInstruction() {
ViewAnimation(duration: 2.5) {
self.instructionLabel?.alpha = 1.0
}.animate()
}
func hideInstruction() {
ViewAnimation(duration: 0.25) {
self.instructionLabel?.alpha = 0.0
}.animate()
}
注意到我们在 show()
方法里停止了计时器。之所以这么做是因为我们希望计时器只被触发一次。你可能会想:“那为什么不用过延时触发显示?”。因为我们希望在提示现实之前用户就打开了菜单的情况下,停止这个计时器。如果用 delay 来实现的话,不能被中途取消。
下一步,创建一个计时器并在 setup()
函数的末尾调用 createInstructionLabel()
:
createInstructionLabel()
timer = Timer(interval: 5.0) {
self.showInstruction()
}
timer?.start()
上述代码设置了在主画布初始化完毕 5 秒后显示菜单,这个时间无论对知道如何通过长按菜单按钮来使用 app 的用户还是初次使用的用户都足够友好。
现在,在 revealMenu()
方法的头部添加 hideInstruction()
的调用, 这里有一个小逻辑,就是只有在提示标签可见的时候,才会执行这一段代码:
if instructionLabel?.alpha > 0.0 {
hideInstruction()
}
同时,在 revealMenu()
方法的顶部添加:
timer.stop()
运行一下:
酷毙了。
7. 阴影
很简单。
在 revealMenu()
方法的顶部添加:
menuShadow.reveal?.animate()
然后,在 hideMenu()
方法的 delay(0.66)
中添加:
self.menuShadow.hide?.animate()
现在,在菜单打开的时候,背景会变暗。
8. 结束
至此,整个菜单的工作已经完成了 98%。下一章讲会把所有东西放到一起,包括为菜单添加交互的事件,信息页面,以及视差效果的背景。
这里是 Menu.swift 的一份拷贝。
喝杯啤酒,休息一下。