视图UIView和层CALayer 窗口和UIView视图是为iOS应用程序构造用户界面的可视化组件。窗口为内容显示提供背景平台,而UIView视图负责绝大部分的内容描画,并负责响应用户的交互。 UIView之所以能够显示,完全是因为其内部的CALayer层对象。UIView真正的绘图部分,就由一个CALayer类来管理,其本身更像是一个CALayer的管理器,访问它跟绘图和坐标有关的属性,例如frame、bounds等,实际上内部都是在访问它所包含的CALayer的相关属性。通过操作这个CALayer对象,可以很方便地调整UIView的一些界面属性,比如阴影、圆角大小、边框宽度和颜色等。 本章将逐步讲解UIView视图及其内部的CALayer层的原理和具体的使用方法。 5.1 视图UIView 5.1.1 UIView概述 UIView是UIKit框架里面最基础的视图类。UIView类定义了一个矩形的区域,并管理该矩形区域内的所有屏幕显示。 在iOS的应用程序中,每个视图对象都要负责渲染视图矩形区域中的内容,并响应该区域中发生的触碰事件。这一双重行为意味着视图是应用程序与用户交互的重要机制。 UIView类定义了视图的基本行为,但并不定义其视觉表现,而是UIKit通过其子类来为文本框、按键及工具条这样的标准界面元素定义具体的外观和行为。 UIView视图类如图5.1所示。 图5.1 这个视图层次可以分为如下几个大类,如表5-1所示。 表5-1 UIKit层次图主要项目说明 项目 说明 NSObject根类 NSObject是一个根类,几乎所有的类都是从它派生而来。根类拥有所有类都有的方法,如alloc和init UIResponsder响应者 UIResponder可以让继承它的类响应移动设备的触摸事件,由于可能有多个对象响应同一个事件,iOS将事件沿响应链向上传递 UIWindow窗口类 UIWindow提供了一个用于管理和显示视图的窗口。窗口提供一个描画内容的表面,是所有其他视图的根容器。每个应用程序通常都只有一个窗口 UIView视图类 UIView视图是所有控件的父类。控件用于响应用户的交互,而UIVIew则负责内容的显示和布局 UIControl控件类 UIControl类几乎是所有交互控件的父类,如按钮,滑块、文本框等。所以UIControl类负责根据触摸事件触发相应的动作 警告视图和动作表单 警告视图和动作表单都可以用于提示用户。它们向用户显示一条消息和一个或多个可选的按键,用户通过这些选项来响应消息 UIView视图和UIWindow窗口 UIView视图和UIWindow窗口是为iOS应用程序构造用户界面的可视组件。窗口为内容显示提供背景平台,而视图负责绝大部分的内容描画与响应用户的交互。 iOS程序启动后,创建的第一个视图控件就是UIWindow,接着创建视图控制器的view,并将该view添加到UIWindow上,于是控制器的view就显示在屏幕上了,如图5.2所示。 图5.2 和桌面mac OS的应用程序有所不同,iOS应用程序通常只有一个窗口,表示为一个UIWindow类的实例。应用程序在启动时创建这个窗口,并往窗口中加入一个或多个视图,然后将它显示出来。窗口一旦显示出来,你基本上就不会再使用到它了,而更多的是对UIView视图的操作。 在iOS应用程序中,窗口对象并没有像关闭框或标题栏这样的区域,所以用户不能直接对其进行关闭或其他操作。 在mac OS中,NSWindow的父类是NSResponder。而在iOS系统中,UIWindow的父类是UIView。因此,UIWindow窗口在iOS系统中也是一个视图对象。 尽管iOS支持多个窗口的存在,但是最好不要创建多个窗口。比如当你希望在自己内容的上方显示警告窗口时,可以使用UIKit提供的警告视图控制器UIAlertController,而不应该再创建新的窗口。 5.1.2 UIView的外观属性 UIView类的外观属性常用的主要有背景颜色、切边、透明度、显示与隐藏。 首先来设置UIView实例的背景颜色。 背景颜色backgroundColor 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 9 let view = UIView(frame: CGRect(x: 40, y: 80, width: 240, height: 240)) 10 view.backgroundColor = UIColor.black 11 self.view.addSubview(view) 12 } 13 14 override func didReceiveMemoryWarning() { 15 super.didReceiveMemoryWarning() 16 // Dispose of any resources that can be recreated. 17 } 18 } 在第9行的代码中,创建了一个位置在(40, 80)、宽度和高度都是240的UIView视图。关于视图的坐标和尺寸等属性,可参阅5.1.3节中的介绍。 接着在第10行代码中,设置视图的backgroundColor背景颜色属性为黑色。UIColor是UIKit中存储颜色信息的一个重要的类,一个UIColor对象包含了颜色和透明度的值,它的颜色空间已经针对iOS进行了优化。UIColor包含了一些类方法用于创建一些最常见的颜色,如白色、黑色、红色、透明色等。 最后通过addSubView()方法,将设置背景色之后的视图添加到当前视图控制器的根视图中。在将项目编译并运行之后,效果如图5.3所示。 除了给视图设置实体颜色之外,还可以将一张图片作为视图的背景颜色: 9 let view = UIView(frame: CGRect(x: 40, y: 80, width: 240, height: 240)) 10 let image = UIImage(named: "Sample") 11 view.backgroundColor = UIColor.init(patternImage: image!) 12 self.view.addSubview(view) 在第10行的代码中,通过UIImage对象加载资源文件夹中的一张图片,然后使用UIColor类的init方法,将加载的图片作为图案平铺在视图的背景中,效果如图5.4所示。 不透明度alpha 接着对代码继续进行修改,以修改视图的不透明度属性alpha。在第10行的代码中,在视图背景颜色的代码下方,继续添加一行代码,用来设置视图的alpha属性: 9 let view = UIView(frame: CGRect(x: 40, y: 80, width: 240, height: 240)) 10 view.backgroundColor = UIColor.black 11 view.alpha = 0.3 12 self.view.addSubview(view) 显示器是由一个个的像素点组成的,每个像素点都可以显示一个由RGBA颜色空间组成的一种颜色值,其中的A就表示透明度alpha。 UIView中的alpha属性是一个浮点值,取值范围在0~1.0,表示从完全透明到完全不透明。alpha属性的默认值为1,当把alpha的值设置成0以后: ? 当前的UIView及其子视图都会被隐藏,而不管子视图的alpha值为多少。 ? 当前的UIView会从响应者链中移除,而响应者链中的下一个会成为第一响应者。 将视图的alpha属性设置为0.3之后,在模拟器中的效果如图5.5所示。 隐藏属性hidden 视图的hidden属性是布尔值类型,用来表示UIView视图是否处于隐藏的状态。接着修改程序的代码为: 9 let view = UIView(frame: CGRect(x: 40, y: 80, width: 240, height: 240)) 10 view.isHidden= true 11 self.view.addSubview(view) 在第10行代码中,将视图的isHidden属性设置为true。 其默认值为false,即视图处于显示状态。当把值设为true时: ? 当前的UIView及其子视图都会被隐藏,而不管子视图的hidden值为多少。 ? 当前UIView会从响应者链中移除,而响应者链中的下一个会成为第一响应者。 在模拟器中的运行结果如图5.6所示。 图5.3 图5.4 图5.5 图5.6 切边属性clipsToBounds 在默认情况下,当向一个视图中添加一个子视图时,如果子视图的区域超出了父视图的范围,子视图超出的部分仍然会在屏幕上正常显示。 如果需要限制子视图的显示范围不超过父视图的显示区域,就需要设置父视图的clipsToBounds属性。首先观察clipsToBounds属性在默认状态下的显示效果: 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 let view = UIView(frame: CGRect(x: 40, y: 80, width: 240, height: 240)) 9 view.backgroundColor = UIColor.black 10 11 let subView = UIView(frame: CGRect(x: 40, y: 40, width: 240, height: 240)) 12 subView.backgroundColor = UIColor.brown 13 view.addSubview(subView) 14 15 self.view.addSubview(view) 16 } 17 18 override func didReceiveMemoryWarning() { 19 super.didReceiveMemoryWarning() 20 // Dispose of any resources that can be recreated. 21 } 22 } 在第8~9行的代码中,创建了一个UIView视图,并设置视图的背景颜色为黑色。 接着在第11~13行的代码中,创建了另一个UIView视图subView,并设置视图的背景颜色为褐色。然后把褐色背景的subView视图添加到view视图中,使subView视图作为黑色背景视图的子视图。 最后在第15行的代码中,将view视图添加到当前视图控制器的根视图中。点击Xcode界面左上角的【编译并运行】按钮 运行该项目,在模拟器中的效果如图5.7所示。 接着在代码中设置view视图的clipsToBounds属性,将该属性的值设置为true: 14 view.clipsToBounds = true 15 self.view.addSubview(view) 再次点击编译并运行按钮,运行该项目,在模拟器中的效果如图5.8所示。 图5.7 图5.8 5.1.3 UIView的几何属性 在为你讲解UIView的几何属性之前,首先了解一下iOS系统的坐标系。 在iOS坐标系统中,坐标的原点位于左上角。如图5.9所示,我们添加了一个UIImageView视图,其原点在(40, 80),宽度为240,高度为340: 1 imgView.frame = CGRect(x: 40, y: 80, width: 240, height: 340) 图5.9 iOS系统包含两个坐标系,其中UIKit坐标系是X轴正方向向右,Y轴正方向向下,而标准的Quartz 2D绘图坐标系为X轴正方向向右,Y轴正方向向上。 下面解释一些相关的概念: ? UIView的frame(origin, size)属性:定义了一个矩形,描述一个UIView的大小和在父坐标系的位置。 ? UIView的bounds(origin, size)属性:同样定义了一个矩形,描述一个UIView的大小和自身坐标系原点的位置。bounds.origin属性默认值是(0, 0),而bounds.size和frame.size是一致的。 ? UIView的center属性:用于确定一个视图的中心点位置,其参照系也是其父视图的坐标系统。在对视图进行放大、缩小或旋转时,该属性的值不会改变。 在下面的代码中,创建了一个简单的UIView,并使用CGRect(x, y, width, height)来定义视图的坐标和尺寸信息。 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 let frame = CGRect(x: 0, y: 0, width: 250, height: 250) 9 let view = UIView(frame: frame) 10 view.backgroundColor = UIColor.black 11 12 let subView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) 13 subView.backgroundColor = UIColor.brown 14 view.addSubview(subView) 15 16 self.view.addSubview(view) 17 } 18 19 override func didReceiveMemoryWarning() { 20 super.didReceiveMemoryWarning() 21 // Dispose of any resources that can be recreated. 22 } 23 } 在第8行的代码中,定义了一个位置在(0, 0)、宽度和高度都是250的区域。然后在第9~10行的代码中,创建了基于该区域的视图对象,并设置背景颜色为黑色。 在第12~14行的代码中,创建了第二个视图对象,并设置背景颜色为褐色。同时将第二个视图作为子视图,添加到第一个视图中。 最后在第16行的代码中,将父视图添加到当前视图控制器的根视图中。点击【编译并运行】按钮 ,运行该项目,在模拟器中的效果如图5.10所示。 接着来设置父视图view的bounds属性,在第10行的代码的下方添加设置语句: 9 let view = UIView(frame: frame) 10 view.backgroundColor = UIColor.black 11 view.bounds = CGRect(x: -50, y: -50, width: 250, height: 250) 完成代码的修改后,再次点击【编译并运行】按钮 运行该项目,在模拟器中的新的效果如图5.11所示。 接着继续修改代码,将第11行的代码修改为: 9 let view = UIView(frame: frame) 10 view.backgroundColor = UIColor.black 11 view.bounds = CGRect(x: -50, y: -50, width: 200, height: 200) bounds属性修改后,它的宽度和高度都变小了。再次点击【编译并运行】按钮 ,运行该项目,在模拟器中的新的效果如图5.12所示。 图5.10 图5.11 图5.12 5.1.4 UIView的嵌套和层次关系 视图可以通过嵌套的方式,组成复杂的层次结构。 例如视图可能包含按钮、标签、图像视图等控件,这些控件则被称为子视图,而包含它们的视图称为父视图。你可以根据项目的业务需求,对视图进行多级嵌套,从而形成复杂的父子视图图层结构。 视图的这种布局方式被称为视图层次,一个视图可以包含任意数量的子视图,通过为子视图添加子视图的方式,可以实现任意深度的嵌套。 视图在视图层次中的组织方式决定了在屏幕上显示的内容,原因是子视图总是被显示在其父视图的上方;这个组织方法还决定了视图如何响应事件和变化。每个父视图都负责管理其直接的子视图,即根据需要调整它们的位置和尺寸,以及响应它们没有处理的事件。 对UIView进行层次管理的方法如表5-2所示。 表5-2 UIView视图层次管理方法列表 方法名称 方法说明 insertSubview(view:, at:) 在指定的位置上插入视图 insertSubview(view:, aboveSubview:) 将视图添加到指定视图的上方 insertSubview(view:, belowSubview:) 将视图添加到指定视图的下方 bringSubview(toFront:) 将指定的子视图移动到最前面 bringSubiew(toBack:) 将指定的子视图移动到最后面 exchangeSubview(at:, withSubviewAt:) 交换两个指定位置的子视图在父视图中的位置 removeFromSuperview 将子视图从父视图中删除 为了演示视图之间的层次转换,我们首先创建三个视图,并将视图依次添加到视图控制器的根视图中。 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 let fisrtView = UIView(frame: CGRect(x: 20, y: 40, width: 200, height: 200)) 9 fisrtView.backgroundColor = UIColor.black 10 11 let secondView = UIView(frame: CGRect(x: 50, y: 70, width: 200, height: 200)) 12 secondView.backgroundColor = UIColor.darkGray 13 14 let thirdView = UIView(frame: CGRect(x: 80, y: 100, width: 200, height: 200)) 15 thirdView.backgroundColor = UIColor.lightGray 16 17 self.view.addSubview(fisrtView) 18 self.view.addSubview(secondView) 19 self.view.addSubview(thirdView) 20 } 21 22 override func didReceiveMemoryWarning() { 23 super.didReceiveMemoryWarning() 24 // Dispose of any resources that can be recreated. 25 } 26 } 在第8~15行的代码中,依次创建了三个视图,它们具有相同的尺寸,但是拥有不同的坐标位置和背景颜色。 接着在第17~19行的代码中,将创建的三个视图依次添加到当前视图控制器的根视图中。点击【编译并运行】按钮 ,运行该项目,在模拟器中的效果如图5.13所示。 观察模拟器中的三个视图的层次关系,然后使用insertSubview(view, belowSubview)方法,调整thirdView和secondView两个视图在父视图中的层次: 17 self.view.addSubview(fisrtView) 18 self.view.addSubview(secondView) 19 self.view.addSubview(thirdView) 20 self.view.insertSubview(thirdView, belowSubview: secondView) 通过在第20行的代码中调用insertSubview方法,将第三个视图插入到第二个视图的下方,最终效果如图5.14所示。 继续修改代码,在第20行的代码下方添加一行代码,调用bringSubview(toFront:)方法,将第一个视图放置在所有子视图的上方: 20 self.view.insertSubview(thirdView, belowSubview: secondView) 21 self.view.bringSubviewToFront(fisrtView) 通过在第21行调用bringSubview(toFront:)方法,第一个视图被放置在其父视图的最顶部的位置,如图5.15所示。 最后再次修改代码,在第21行的代码下方添加一行代码,调用removeFromSuperview()方法,将第一个视图从其父视图中删除: 21 self.view.bringSubview(toFront: fisrtView) 22 fisrtView.removeFromSuperview() 通过调用firstView视图的removeFromSuperview()方法,firstView视图将从父视图中移除,效果如图5.16所示。 图5.13 图5.14 图5.15 图5.16 5.1.5 UIView的交互属性 通过设置UIView的userInteractionEnabled属性,可以激活用户的交互特性。该属性值为布尔类型,由属性本身的名称可知,该属性决定UIView是否接受并响应用户的交互。 当该属性的值为false时,UIView会忽略那些发生在其自身的诸如触摸和键盘等用户事件,并将这些事件从消息队列中移除出去;当值为true时,这些用户事件会正常派发至UIView本身,UIView会按照之前注册的事件处理方法来响应这些事件。 下面通过一个实例,讲解如何给视图添加交互事件。 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 9 let touchView = UIView(frame: CGRect(x: 60, y: 60, width: 200, height: 200)) 10 touchView.backgroundColor = UIColor.black 11 self.view.addSubview(touchView) 12 13 let guesture = UITapGestureRecognizer(target: self, action: #selector(ViewController.SingleTap)) 14 touchView.addGestureRecognizer(guesture) 15 } 16 17 func SingleTap() 18 { 19 print("You touched me.") 20 } 21 22 override func didReceiveMemoryWarning() { 23 super.didReceiveMemoryWarning() 24 // Dispose of any resources that can be recreated. 25 } 26 } 在第9~11行的代码中,创建了一个简单的黑色背景的UIView视图,并将视图添加到当前视图控制器的根视图。 接着在第13行的代码中,创建了一个UITapGestureRecognizer手势对象,手势对象的详细讲解可参考第9章课程。 手势创建完成后,在第14行的代码中,通过addGestureRecognizer方法,将手势对象指定给视图对象。当用户点击该视图时,将调用手势定义的回调方法SingleTap()。 最后在第17~20行的代码中,实现手势的回调方法SingleTap(),当用户点击手势绑定的视图时,将在控制台输出一条日志语句。 完成代码的编写之后,点击【编译并运行】按钮 ,运行该项目,项目在模拟器中的效果如图5.17所示。在模拟器中点击黑色的视图对象,并观察Xcode界面的右下角控制台的日志输出。 5.1.6 UIView的变形操作 CGAffineTransform仿射转换结构体代表了一种用于仿射变换的矩阵。 结构体的参数指定了从一个坐标系的点转化成另外一个坐标系的点的规则。你可以通过仿射转换功能,对一个视图的坐标系统进行一些变换,从而实现视图的缩放、旋转、位移等功能。 仿射变换是一种特殊类型的映射,保留在一个路径中的平行线,但不一定保留长度或角度。缩放、旋转、平移是最常用的仿射变换。我们通常不会直接创建一个仿射变换,只需要根据现有的参数,修改现有的仿射变换即可。 常用的几种仿射变换见表5-3所示。 表5-3 几种常用的仿射变换 名称 说明 CGAffineTransformMakeTranslation 通过指定的x, y值,创建一个平移矩阵 CGAffineTransformTranslate 对已存在的矩阵进行平移 CGAffineTransformMakeRotation 通过指定角度来创建一个旋转矩阵 CGAffineTransformRotate 对已存在的矩阵进行旋转 CGAffineTransformMakeScale 通过指定的x, y缩放因子,创建一个缩放矩阵 CGAffineTransformScale 对已存在的矩阵进行缩放 CGAffineTransformInvert 反转矩阵,将值与反转矩阵相乘得到原先的值 CGAffineTransformConcat 对仿射效果进行叠加操作 平移仿射变换 接着我们来编写代码,使用仿射变换对视图进行位移、缩放、放置和斜切的变换操作。 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 50)) 9 view.center = self.view.center 10 view.backgroundColor = UIColor.black 11 self.view.addSubview(view) 12 13 let transform = view.transform 14 view.transform = transform.translateBy(x: 0, y: 100) 15 } 16 17 override func didReceiveMemoryWarning() { 18 super.didReceiveMemoryWarning() 19 // Dispose of any resources that can be recreated. 20 } 21 } 在第8~9行的代码中,创建了一个位于{0, 0}、宽度为200、高度为50的UIView视图。接着设置视图的center属性,将视图的center属性设置为与当前视图控制器根视图的center相同的位置。由于仿射变换是以视图的center为基点的,所以将视图放置在屏幕的中心位置,方便对仿射变换操作效果进行观察。 在第10~11行的代码中,将视图的背景颜色设置为黑色,并添加到当前视图控制器的根视图中。该视图在未使用仿射变换前的效果,如图5.18所示。 在第13~14行的代码中,首先获得视图对象的transform属性,然后使用仿射变换的transform.translateBy(x:, y:)方法,对视图的仿射矩阵进行平移操作。其中transform表示现有的矩阵,参数x表示在水平方向上平移的距离,参数y表示在垂直方向上平移的距离。这里是在垂直方向上,将视图的矩阵向下平移了100。 点击【编译并运行】按钮 ,运行该项目,项目在模拟器中的效果如图5.19所示。 缩放仿射变换 接着来使用transform.scaleBy(x: , y: )方法,对视图的矩阵进行缩放操作。其中transform表示现有的矩阵,参数x表示在水平方向上缩放的比例,参数y表示在垂直方向上缩放的比例。 将第14行的代码修改为: 14 view.transform =transform.scaleBy(x: 1.5, y: 1.5) 对矩阵的缩放操作,本质上是拉长或缩短原来矩阵中的点与点之间的距离。这里是通过transform对象的scaleBy(x: , y: )方法,将视图在水平和垂直方向上各放大了1.5倍。最终的效果如图5.20所示,同时与图5.18的原始效果进行比较。 旋转仿射变换 接着来使用transform.rotate(angle)方法,对视图的矩阵进行旋转操作。其中transform表示现有的矩阵,参数angle表示在顺时针方向上旋转的角度。 14 view.transform = transform.rotate(3.14/4) 这里通过transform的rotate(angle)方法,将视图沿顺时针旋转45度,最终效果如图5.21所示。 斜切仿射变换 在上面进行的平移、缩放、旋转操作中,都是采用系统封装好的方法。例如我们使用transform的rotate(angle)方法传入的angle参数,就是对CGAffineTransformMake方法的6个参数的封装。 系统并没有提供类似于CGAffineTransformRotate方法的斜切操作方法。所以这里我们将使用CGAffineTransformMake(CGFloat sx, CGFloat shx, CGFloat shy, CGFloat sy, CGFloat tx, CGFloat ty)方法,自定义视图的变换,从而实现对视图进行斜切操作。 CGAffineTransformMake方法中的6个参数的含义如表5-4所示。 表5-4 CGAffineTransformMake方法参数列表 参数名称 参数说明 参数名称 参数说明 sx 水平方向上的缩放因子 shy 垂直方向上的斜切因子 sy 垂直方向上的缩放因子 tx 水平方向上的位移因子 shx 水平方向上的斜切因子 ty 垂直方向上的位移因子 在Swift 3.0中,CGAffineTransformMake方法已经被取消,但是CGAffineTransform包含6个属性a、b、c、d、tx、ty,分别对应于CGAffineTransformMake方法的sx、shx、shy、sy、tx、ty 6个参数,因此我们可以通过设置这6个参数的值,来达成和使用CGAffineTransformMake方法一样的效果。 这里将第14行的代码修改为: 14 view.transform.a = 1.0 15 view.transform.b = 0.5 16 view.transform.c = 0.5 17 view.transform.d = 1.0 18 view.transform.tx = 0.0 19 view.transform.ty = 0.0 在设置transform的过程中,其参数a、d的值为1,即保持缩放比例不变;其tx、ty属性为0,即在水平和垂直方向上不进行平移;其b和c属性都为0.5,即在水平和垂直方向上进行斜切操作。斜切的效果如图5.22所示。 图5.18 图5.19 图5.20 图5.21 图5.22 5.1.7 自定义UIView视图 在iOS开发工作中,经常会使用到自定义的UIView视图。使用自定义视图,可以很方便地复用一些复杂或不规则的视图对象。 在创建自定义UIView视图前,首先往项目中添加一个Swift文件,如图5.23所示。 接着在弹出的模板选择窗口中,依次选择【iOS > Source > Swift File】选项,创建一个新的Swift类文件,如图5.24所示。 图5.23 图5.24 Swift文件创建后的效果如图5.25所示。 图5.25 编写RoundView自定义视图 现在开始编写代码,实现RoundView.swfit自定义视图。 1 import UIKit 2 class RoundView: UIView 3 { 4 var color = UIColor.blue 5 override init(frame: CGRect) 6 { 7 super.init(frame:frame) 8 self.backgroundColor = UIColor.clear 9 } 10 11 override func draw(_ rect: CGRect) 12 { 13 let ctx = UIGraphicsGetCurrentContext() 14 ctx?.clear(self.frame) 15 ctx?.setFillColor(color.cgColor) 16 ctx?.fillEllipse(in: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)) 17 } 18 19 required init?(coder aDecoder: NSCoder) 20 { 21 fatalError("init(coder:) has not been implemented") 22 } 23 } 在第2行的代码中,使用class关键字,创建了一个名为RoundView的类,该类继承自UIView父类。RoundView类的主体功能被放置在一对大括号之间。 在第4行的代码中,给RoundView类添加了一个UIColor类型的属性color,并设置color属性的默认值为蓝色。 接着在第5~9行的代码中,重写了RoundView自定义类的初始化init方法。在该初始化方法中,首先调用父类的初始化方法,然后自定义设置背景颜色为无色,从而保证RoundView只显示在drawRect方法中创建的内容。 在第11~17行的代码中,重写了drawRect方法。绘制一个UIVIew最灵活的方式就是由它自己完成绘制。实际上你并没有绘制一个UIView,而只是子类化了UIView,并赋予子类绘制自己的能力。当一个UIVIew需要执行绘图操作时,drawRect:方法就会被调用。覆盖此方法可让你获得绘图操作的机会。当drawRect:方法被调用时,当前的图形上下文也被设置为属于视图的图形上下文,可以使用Core Graphics或UIKit提供的方法将图形画到该上下文中。 所以在第13行的代码中,通过UIGraphicsGetCurrentContext方法,获得当前的图形上下文。然后调用ctx上下文的clear方法擦除一个区域,这个函数会擦除一个矩形范围内的所有已存在的绘图内容。 接着调用ctx上下文的setFillColor方法,设置在图形上下文中的填充颜色为当前视图属性color的颜色。 最后在第16行的代码中,调用ctx上下文的fillEllipse方法,在当前的图形上下文中,在(0,0)位置绘制与当前视图相同尺寸的椭圆。 当视图的宽度和高度相同时,绘制的图形为正圆。 使用自定义视图 完成自定义视图的创建后,打开ViewController.swift文件,在此文件中创建刚刚自定义的视图,并将自定义视图添加到当前的视图控制器的根视图中。 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 9 let view = RoundView(frame: CGRect(x: 40, y: 40, width: 240, height: 240)) 10 self.view.addSubview(view) 11 } 12 13 override func didReceiveMemoryWarning() { 14 super.didReceiveMemoryWarning() 15 // Dispose of any resources that can be recreated. 16 } 17 } 在第9行的代码中,创建了一个自定义的RoundView视图,其位置在(40, 40),宽度和高度都是240。接着在第10行的代码中,将视图添加到当前视图控制器的根视图中。 接着点击【编译并运行】按钮 ,运行该项目。项目在模拟器中的效果如图5.26所示。 由于我们给RoundView自定义视图添加了color属性,所以你可以自定义圆形的颜色。 在第9行的代码下方添加一行代码,将圆形的背景颜色修改为绿色: 10 view.color = UIColor.green 修改后的效果如图5.27所示。 接着我们再修改第9行的代码,将自定义视图的高度设置为140: 9 let view = RoundView(frame: CGRectMake(40, 40, 240, 140)) 自定义视图的高度变小后,将由正圆转换为椭圆,如图5.28所示。 图5.26 图5.27 图5.28 5.2 CALayer层 UIView是iOS系统中界面元素的基础,所有的界面元素都继承自它,它本身完全是由CoreAnimation来实现的。 而UIView真正的绘图部分,是由一个叫CALayer(Core Animation Layer)的类来管理的。 UIView本身更像是一个CALayer的管理器,访问它的跟绘图和跟坐标有关的属性,例如frame,bounds等,实际上内部都是在访问它所包含的CALayer的相关属性。 UIView与CALayer的关系如图5.29所示。 图5.29 5.2.1 CALayer边框 通过设置CALayer的borderWidth和borderColor属性,可以给视图添加边框效果: 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 let view = UIView(frame: CGRect(x: 60, y: 60, width: 200, height: 200)) 9 view.backgroundColor = UIColor.black 10 view.layer.borderWidth = 20 11 view.layer.borderColor = UIColor.lightGray.cgColor 12 13 self.view.addSubview(view) 14 } 15 16 override func didReceiveMemoryWarning() { 17 super.didReceiveMemoryWarning() 18 // Dispose of any resources that can be recreated. 19 } 20 } 在第8~9行的代码中,创建了一个位于(60,60)、宽度和高度都是200的视图,并设置视图的背景颜色为黑色。 接着在第10行的代码中,设置层的边缘宽度borderWidth为20。 然后在第11行的代码中,设置层的边缘颜色borderColor的值为浅灰色。这里使用的是CGColorRef数据类型,而不是UIColor。这是因为UIColor是定义在UIKit框架中的,只能在iOS中使用。而CALayer是定义在QuartzCore框架中的,所以需要使用具有跨平台特性的CGColorRef数据类型。通过调用UIColor对象的CGColor属性,可以获得UIColor转换后的CGColorRef值。 接着点击Xcode界面左上角的【编译并运行】按钮 ,运行该项目。项目在模拟器中的效果如图5.30所示。 5.2.2 CALayer阴影 通过设置CALayer的几个阴影属性,可以给视图添加阴影效果,这样即使不需要使用Photoshop等图像处理软件,也能够实现投影效果了。 1 class ViewController: UIViewController { 2 3 override func viewDidLoad() { 4 super.viewDidLoad() 5 // Do any additional setup after loading the view, typically from a nib. 6 let view = UIView(frame: CGRect(x: 60, y: 60, width: 200, height: 200)) 7 view.backgroundColor = UIColor.black 8 9 view.layer.shadowColor = UIColor.black.cgColor 10 view.layer.shadowOffset = CGSize(width: 10.0, height: 10.0) 11 view.layer.shadowOpacity = 0.45 12 view.layer.shadowRadius = 5.0 13 14 self.view.addSubview(view) 15 } 16 17 override func didReceiveMemoryWarning() { 18 super.didReceiveMemoryWarning() 19 // Dispose of any resources that can be recreated. 20 } 21 } 在第8~9行的代码中,创建了一个位于(60,60)、宽度和高度都是200的视图,并设置视图的背景颜色为黑色。 接着在第9~12行的代码中,设置了层的阴影属性。首先在第9行的代码中,设置了shadowColor阴影颜色为黑色。然后在第10行的代码中,设置阴影的偏移值为(10.0, 10.0),即在水平方向上向右侧偏移10,在垂直方向上向下偏移10。shadowOffset的默认值为(0.0, -3.0)。 在第11行的代码中,设置阴影的不透明度值为0.45。该属性的取值范围为0~1,即从完全透明到完全不透明。shadowOpacity的默认值为0.0。 最后在第12行的代码中,设置阴影的shadowRadius模糊半径为5.0,用来实现阴影的模糊效果,使阴影更加柔和、自然。shadowRadius参数的默认值为3.0。 接着点击Xcode界面左上角的【编译并运行】按钮 ,运行该项目。项目在模拟器中的效果如图5.31所示。 5.2.3 CALayer圆角 通过设置CALayer的cornerRadius属性,可以给视图添加圆角效果: 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 let view = UIView(frame: CGRect(x: 60, y: 60, width: 200, height: 200)) 9 view.backgroundColor = UIColor.black 10 view.layer.cornerRadius = 40 11 12 let subView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 100)) 13 subView.backgroundColor = UIColor.gray 14 15 view.addSubview(subView) 16 self.view.addSubview(view) 17 } 18 19 override func didReceiveMemoryWarning() { 20 super.didReceiveMemoryWarning() 21 // Dispose of any resources that can be recreated. 22 } 23 } 在第8~9行的代码中,创建了一个位于(60,60)、宽度和高度都是200的视图,并设置视图的背景颜色为黑色。 接着在第10行的代码中,设置层的cornerRadius圆角半径为40。当该属性被设置为大于0的值时,将会在层的四周绘制指定半径的圆角。 在第12~13行的代码中,创建了一个宽度为200、高度为100、位于(0, 0)的视图,该视图的背景颜色为灰色。 最后在第15~16行的代码中,将灰色视图作为子视图,添加到黑色视图之中。并将黑色视图添加到当前视图控制器的根视图中。 接着点击Xcode界面左上角的【编译并运行】按钮 ,运行该项目。项目在模拟器中的效果如图5.32所示。 从模拟器中的效果可以看出,虽然给黑色视图添加了圆角效果,但是由于子视图的存在,你无法看到上方两个顶点的圆角效果。这是因为圆角效果只对视图的背景颜色和层的边框起作用,而不会对层中的内容起作用。不过系统提供了一个属性masksToBounds,你可以将该属性的值设置为true,这样将会沿着圆角边缘对视图中的内容进行裁切。 在第10行的代码的下方添加一行新的代码: 8 let view = UIView(frame: CGRectMake(60, 60, 200, 200)) 9 view.backgroundColor = UIColor.blackColor 10 view.layer.cornerRadius = 40 11 view.layer.masksToBounds = true 设置层的masksToBounds属性之后,再次运行模拟器,视图的圆角效果如图5.33所示。 在设置圆角半径时,如果将cornerRadius设置为正方形宽度的一半,那么将会创建了一个正圆形。这里将第10行的代码修改为: 10 view.layer.cornerRadius = 100 修改cornerRadius半径数值后的效果如图5.34所示。 也许你还想知道,如果继续增加cornerRadius半径数值,会有什么样的圆角效果。我们将cornerRadius半径数值设置为与正方形宽度相同: 11 view.layer.cornerRadius = 200 将cornerRadius半径数值修改为200后的效果如图5.35所示。 5.2.4 CALayer渐变 CALayer和UIView相似地方是,CALayer层也可以嵌套多个子CALayer层,从而实现多种多样的效果。 这里将演示如何往层中添加一个CAGradientLayer渐变层。CAGradientLayer是用来生成两种或更多的颜色平滑渐变效果的。 图5.32 图5.33 图5.34 图5.35 1 import UIKit 2 3 class ViewController: UIViewController { 4 5 override func viewDidLoad() { 6 super.viewDidLoad() 7 // Do any additional setup after loading the view, typically from a nib. 8 let rect = CGRect(x: 20, y: 60, width: 240, height: 240) 9 let gradientView = UIView(frame: rect) 10 11 let gradientLayer = CAGradientLayer() 12 gradientLayer.frame = gradientView.frame 13 14 let fromColor = UIColor.yellow.cgColor 15 let midColor = UIColor.blue.cgColor 16 let toColor = UIColor.red.cgColor 17 18 gradientLayer.colors = [fromColor, midColor, toColor] 19 gradientLayer.startPoint = CGPoint(x: 0, y: 0) 20 gradientLayer.endPoint = CGPoint(x: 1, y: 1) 21 gradientLayer.locations = [0, 0.3, 1] 22 23 gradientView.layer.addSublayer(gradientLayer) 24 self.view.addSubview(gradientView) 25 } 26 27 override func didReceiveMemoryWarning() { 28 super.didReceiveMemoryWarning() 29 // Dispose of any resources that can be recreated. 30 } 31 } 在第8~9行的代码中,创建了一个位于(20,60)、宽度和高度都是240的视图。 接着在第11~12行的代码中,创建一个CAGradientLayer渐变层,并设置渐变层的frame属性与视图的frame属性相同。 然后在第14~16行的代码中,依次创建三个颜色:黄色、蓝色和红色。这三个颜色将作为渐变线上的起始颜色、中间颜色和结束颜色。 在第18行的代码中,设置了渐变层的colors属性,这里将刚刚创建的三个颜色放在一个数组中,并赋予colors属性。 在第19行的代码中,设置了渐变层的起点位置为(0, 0),即渐变线的起点位于渐变层的左上角。 接着在第20行的代码中,设置了渐变层的终点位置为(1, 1),即渐变线的终点位于渐变层的右下角。这样就创建了一个从左上角至右下角45度方向的渐变效果。 在第21行的代码中,设置了渐变层的各颜色点在颜色线中的分布情况。在locations属性值数组中的0.3表示渐变线中间的颜色,即蓝色将位于渐变线30%的位置。 在第23~24行的代码中,依次将渐变图层添加到视图对象的根层中,然后将视图对象添加到当前视图控制器的根视图中。 接着点击Xcode界面左上角的【编译并运行】按钮 ,运行该项目。项目在模拟器中的效果如图5.36所示。 从图中的效果可以看出,我们创建了一条从左上角至右下角的渐变效果,渐变色依次为黄色、蓝色、红色。其中蓝色位于渐变线30%的位置,即比较靠近渐变线开始位置的黄色,而距离渐变线结束位置的红色较远。 5.3 小 结 我们经常会在网上冲浪,网页中的各种视觉元素基本上都是由Div容器组成的。就像网页中的Div标签一样,iOS设备中的大部分视觉元素也都是通过UIView视图组成的。本章主要讲解了UIView视图的基本属性、各个属性之间的关系,以及如何在应用程序中创建和操作这些属性。 除此之外,本章还讨论了视图如何响应触摸事件,以及如何创建自定义视图以描绘定制的内容。使用UIView甚至可以制作出很多精美的动画效果,我们将在第10章详细讨论UIView动画的设计和制作。 在讲解UIView视图时,始终绕不开CALayer层概念,所以在本章还讲解了CALayer层和UIView视图之间的联系,以及如何使用CALayer构建一些诸如圆角、阴影、渐变之类的外观效果。