The HUD
我还打算对这个界面做一个很重要的改进,当你点击Done按钮关闭这个界面时,弹出一个小窗口,显示Tagged,告诉用户操作成功了。这样的小细节会使用户体验非常棒。
这个覆盖一个界面的功能通常被叫做HUD(Heads-Up Display,又是一个不太好翻译的术语)。实际上HUD经常被用来显示进度条或者转轮一样的那种读取界面,比如下载的时候,或者注册用户后提交等待的那几秒钟。
在这个界面关闭前,你会显示一个HUD来反馈用户,这样会使得app丰满许多。
HUD其实就是一个UIView的子类,你可以通过它在一个视图上显示另一个视图。其实你已经见识过很多次了。
比如把label放到cell上面,和HUD的概念就差不多,cell本身也是在table view上面的,而table view自己则是导航控制器中最顶层的一个视图。
目前为止,你都只是需要视图控制器或者数据模型的时候才创建自己的对象,但是其实,你也可以创建自己的视图。
通常,使用元件库中的button或者标签就已经足够了,但是假如你需要一点自己的特色功能时,就需要自己创建一个视图了。你可以通过继承UIView或者UIControl来完成这个工作。
添加一个新的Swift文件,命名为HudView。
我们先来做一个简易版的HudView,让你先了解下情况,然后我们再完善它。
将HubView.swift中的内容都删掉,替换为下面的代码:
import UIKit
class HudView: UIView {
var text = ""
class func hud(inView view: UIView,animated: Bool)-> HudView {
let hudView = HudView(frame: view.bounds)
hudView.isOpaque = false
view.addSubview(hudView)
view.isUserInteractionEnabled = false
hudView.backgroundColor = UIColor(red:1,green:0,blue:0,alpha:0.5)
return hudView
}
}
这里的hud(inView,animated)方法叫做便利构造方法(convenience constructor)。它创建并且返回一个新的HudView实例。
通常,我们是这样创建实例的:
let hudView = HudView()
通过便利构造方法,你可以这样创建实例:
let hudView = HudView.hud(inView: parentView, animated: true)
便利构造方法通常都是类方法,它的前面有个class前缀,可以作为一个独立的整体,而不是特定的实例。
当你调用HudView.hud(inView: parentView, animated: true)后并不会拥有一个HudView实例,它的作用是由这个方法为你创建一个HUD view并且把它放到其它视图的顶层,而不是由你亲自去做。
你可以看到,在这个方法的内部第一行就是创建实例:
class func hud(inView view: UIView, animated: Bool) -> HudView {
let hudView = HudView(frame: view.bounds)
...
return hudView
}
第一行调用了HudView()或者说是HudView(frame),这是一个从UIView中继承来的初始化方法。在整个方法的最后一个新的HUD实例被返回给调用者。
那么我们为什么要用便利构造方法呢?顾名思义,当然是为了方便。
初始化一个视图需要很多步骤,但是如果把这些事都放在便利构造方法中,那么调用者就不用操心这么多了。
其中有一个步骤是把HudView对象作为子视图,添加到它的父视图的顶层,这里父视图就是导航控制器视图,所以HUD会把父视图全部覆盖掉。
同时我们将isUserInteractionEnabled(是否与用户交互)属性设置为false。就是说当HUD视图显示现在界面上的时候,用户点击它不会发生任何事情。
我们来简单测试一下,现在HUD视图的背景颜色是红色,50%透明度。这样便于你观察它是否真的显示出来了,并且覆盖了整个屏幕。
打开LocationDetailsViewController.swift,将done方法修改为:
@IBAction func done() {
let hudView = HudView.hud(inView: navigationController!.view, animated: true)
hudView.text = "Tagged"
}
这样就创建了一个HudView对象并且把它添加到导航控制器视图中去了,还自带一点小动画。同时设置了text属性。
之前,done方法会自动关闭界面,但是为了测试观察效果,我们暂时不需要这样做。如果界面关闭了,你就无法观察HUD是否显示了。测试好后,我们稍后会把这个功能重新添加上。
运行app,看看效果是不是很惊艳:
现在app完全对用户的点击失去响应了。
在操作视图的时候,先给他一个亮色的背景是个非常好的办法,这样便于你观察效果。
打开HudView.swift,把backgroundColor这一行从hud(inView: animated)方法中移除。
然后添加下面这个方法进去:
override func draw(_ rect: CGRect) {
let boxWidth: CGFloat = 96
let boxHeight:CGFloat = 96
let boxRect = CGRect(x: round((bounds.size.width - boxWidth) / 2), y: round((bounds.size.height - boxHeight) / 2), width: boxWidth, height: boxHeight)
let roundedRect = UIBezierPath(roundedRect: boxRect, cornerRadius: 10)
UIColor(white: 0.3,alpha: 0.8).setFill()
roundedRect.fill()
}
draw()方法会在UIKit需要你的视图重绘自己的时候被调用。
回忆一下,我们前面说过,iOS中的所有东西都是事件驱动的。视图不会主动在界面上被绘制,除非UIKit发送了draw()事件。这就是说你永远不应该自己去调用draw()。
如果你需要重绘一个视图的话,你应该发送setNeedsDisplay()消息。然后UIKit会在适合的时候发送draw()方法。如果你以前接触过其它平台,也许会对此感到不解,在其它平台上,通常是你自己掌管界面的绘制,但是在iOS中,UIKit掌管这些事。
你刚才添加的draw()在界面的中心绘制了一个圆角矩形,边长为96和96,很明显这是一个正方形。
let boxWidth: CGFloat = 96
let boxHeight:CGFloat = 96
这里声明了两个常量,之后你会用它们参与相关的计算。它们的命名非常直观,一看就明白是什么意思。
注意胰腺癌,这些常量的类型都被强制转换为了CGFloat,因为UIKit用的就是这种类型。当处理UIKit和Core Graphics时,都会用CGFloat来代替Float和Double。
let boxRect = CGRect(x: round((bounds.size.width - boxWidth) / 2), y: round((bounds.size.height - boxHeight) / 2), width: boxWidth, height: boxHeight)
这里又见到了CGRect,它是用来代表矩形的一个结构。你使用它来计算HUD的位置。HUD应该在界面上是水平居中和垂直居中的。界面的宽度和高度可以用bounds.size.width和bounds.size.height得到,所以用(bounds.size.width - boxWidth) / 2就正好是水平居中了。
使用round函数的作用是保证不要出现小数,这样会使得视图很模糊。
let roundedRect = UIBezierPath(roundedRect: boxRect, cornerRadius: 10)
UIColor(white: 0.3,alpha: 0.8).setFill()
roundedRect.fill()
UIBezierPath对绘制矩形的圆角非常有帮助。你只需要输入一个矩形作为参数,再告诉他圆角的弧度就可以了。
然后你设置颜色为灰色,并且透明度为80%。
运行app,效果应该是这个样子的:
我们还要在HUD中添加两样东西,一个对勾符号和一个文本标签,其中对勾符号是一个图片文件。
在本书附带的资源文件夹中的Hud Images文件夹中有两个文件,Checkmark@2x.png和Checkmark@3x.png。把这两个文件放入到资产文件夹中(Assets.xcassets),和之前导入图标的操作方式一样。
在draw()方法的底部添加如下代码:
if let image = UIImage(named: "Checkmark") {
let imagePoint = CGPoint(
x: center.x - round(image.size.width / 2),
y: center.y - round(image.size.height / 2) - boxHeight / 8)
image.draw(at: imagePoint)
}
这段代码读取checkmark图片文件到UIImage对象中。然后计算该图片的坐标,正好把它放到HUD视图的中心。
运行app,你会看到下面的效果:
可失败的初始化(Failable initializers)
在创建UIImage时你使用了if let来进行解包。这是因为UIImage(named)是一种可失败的初始化。
因为读取图片可能会失败,比如图片不存在或者指定的名称根本不是一个图片文件。你不能糊弄UIImage,让他读取一个非图片文件。
事实上UIImage的init方法定义为init?(named),这里的问号说明了,这个方法的返回值是一个可选型。如果没有读取到有效的图片的话,它会返回一个nil。
在iOS框架中,你会经常看到这种可失败的初始化,比如你曾经遇到过的init?(coder),这种方法的特点就是无论对象是否创建成功,它都会有所返回,所以你必须先对它们进行解包才能使用。
同上在视图上放置文本都会使用UILabel对象,然而对于这种简单的视图,你可以自己创建文本对象。
在draw()方法底部添加以下代码:
let attribs = [ NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16),
NSAttributedStringKey.foregroundColor: UIColor.white ]
let textSize = text.size(withAttributes: attribs)
let textPoint = CGPoint(
x: center.x - round(textSize.width / 2),
y: center.y - round(textSize.height / 2) + boxHeight / 4)
text.draw(at: textPoint, withAttributes: attribs)
以上代码已经针对Swift 4优化过了。
当你绘制一个文本的时候,首先你要知道这个文本的大小,这样你才能准确计算它的位置。String有很多方法可以实现这个目的。
首先你创建了一个用于文本的UIFont对象,字体是“System”,大小是16。
同时你还指定了文本的颜色为white(白色)。字体和前景颜色被放入了一个名为attribs的字典中。
回忆一下,字典存储的是键值配对关系。创建一个字典的基本语法是:
let myDictionary = [ key1: value1,
key2: value2,
key3: value3 ]
回到draw()方法中的attribs字典,其中的NSAttributedStringKey.font关联的是UIFont对象,NSAttributedStringKey.foregroundColor关联的是UIColor对象。换而言之attribs字典描述了文本的字体和颜色。
然后你使用这些属性和来自文本中的字符串的属性来计算文本的宽和高,并把结果存放在常量textSize中,这个常量的类型是CGSize。(在你制作自己的视图时,CGPoint、CGSize、CGRect这些类型都是你的好朋友)
最后你计算出文本的位置,并且将结果存放在textPoint中,并且将它最终绘制到屏幕上,整个过程非常简单。
运行app,现在看起来就高大上了许多。
你可以在模拟器中的多种设备类型上测试,无论设备的尺寸如何,HUD视图总是在屏幕的中央。
但是我们的要求要更高一点,我们来添加一点小动画,让这个过程更加生动。这非常简单。
在HudView.swift文件中添加show(animated)方法:
func show(animated: Bool) {
if animated {
//1
alpha = 0
transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
//2
UIView.animate(withDuration: 0.3, animations: {
//3
self.alpha = 1
self.transform = CGAffineTransform.identity
})
}
}
在Bull's Eye这个课程中,你使用Core Animation框架做了一点动画。但是UIView有自己的动画机制。后台用的也是Core Animation,但是使用的方法要简单一些。
它的固定步骤是:
1、在动画开始前设置视图的初始状态。这里你将alpha设置为0,使界面完全透明。同时设置transform的比例因子为1.3。在这里我们不深入讨论这个概念。
2、调用UIView.animate(withDuration:...)设置动画。你在闭包中定义了动画,回忆一下闭包是一个不会立即执行的代码块。UIKit将这些属性在闭包内从初始状态改变到最终状态,并且将其动画化。
3、在闭包内部,你设置了动画结束时视图的最终状态。alpha为1,就是说HudView视图此时完全不透明。同时将比例因子设置为正常状态。因为这些代码时写在闭包中的,所以引用这些属性必须使用self关键字。
HUD视图的透明度从完全透明到完全不透明转而迅速消失,并将从原始尺寸的1.3倍缩小到其常规宽度和高度。
这是一段非常简单的动画,但是也非常漂亮。
在hud(inView, animated)方法中调用show(animated)方法,就放在return语句的前面。
class func hud(inView view: UIView, animated: Bool) -> HudView { ...
hudView.show(animated: animated)
return hudView
}
运行app,感动一下自己。
你可以试试一种叫做弹簧的动画效果,非常有意思,也非常简单。
将UIView.animate(withDuration:...)方法替换为下面这个样子:
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5, options: [], animations: {
self.alpha = 1
self.transform = CGAffineTransform.identity
},completion: nil)
运行app,其实这些动画的效果非常微妙,但是正是这种细节给用户带来了良好的体验。