7.2 视图UIView
7.2.1 UIView简介
UIView是所有界面UI类控件的父类。UIView类的对象负责屏幕上一个矩形区域的显示和行为动作。例如UIButton、UIImageview等都继承自UIView,因此,UIView所具备的属性和方法,其子类也都同样具备。
1.UIView的原理
UIView类(视图类)负责管理屏幕上的一块矩形区域,包括这个区域内的显示样式,比如背景颜色、大小、以及行为动作,例如监测用户点击等触碰事件。
视图还可以用于管理一个或者多个子视图。用户看到的某个样式,有可能是多个视图叠加后的显示效果。视图的这种布局方式,也称为视图层次,一个父视图可以包含任意多个子视图。同时,父视图的属性有时也会影响到子视图的样式以及用户交互行为。
总体来讲,视图类的主要作用有以下3个方面。
- 样式显示与动画:负责自身矩形区域内样式的显示,以及某些属性(大小、位置、角度)变化时的动画过渡效果。
- 布局与子视图管理:管理子视图。
- 事件处理:接收并响应用户的触摸事件。
在iOS开发中,UIView与UIViewController紧密协作,UIViewController负责UIView的加载与卸载。有关UIViewController的介绍,可以参考后续UIViewController的相关教程。
2.UIView的父类与子类
通过UIKit的族图可以看到,UIView继承自UIResponder,因此UIView可以响应用户交互。另外,一些常用控件都继承自UIView。需要特别说明的是,窗口(UIWindow)也是继承自UIView,窗口可以认为是一个特殊的View,如图7-2所示。
3.最常使用的UIView属性
在iOS开发中,有些UIView的属性是经常用的,在此给大家简单列举一些,后面课程中会做详细讲解。
- UIView有关样式的常用属性。
- UIView管理子视图的常用属性和方法。
图7-2 UIView的父类与子类
- UIView有关动画的属性和方法。
- UIView有关交互的常用属性与方法。
7.2.2 视图UIView的外观
在手机上呈现给用户的视图之所以其样式是多种多样的,是因为UIView类中有很多关于视图样式定制的属性,例如大小、位置、透明度等。修改这些属性,就可以对视图的样式进行修改,修改的过程还可以展现出多样的动画效果。
1.基本样式:背景颜色、透明度以及是否隐藏
背景颜色属性BackgroundColor属性是UIView类中最常使用的属性之一。由于UIView是一个矩形区域,所以在实际的开发过程中,常常通过设置视图的背景颜色来检查视图的大小以及位置。
透明度alpha属性可以修改视图的透明度,实现一些虚化的效果,例如,在一些游戏App中,游戏的按钮经常是虚化的效果。需要注意的是对于UIView以及其子类,当alpha的值≤0.01时,就不能够再响应用户交互了。
是否隐藏hidden属性能够控制视图的显示与隐藏。
2.位置与大小:Frame、Bounds、Center
Frame、Bounds以及Center是用来设置视图对象位置以及大小的属性,如图7-3所示。在对任何视图类对象进行初始化之后,建议大家随即就要去设置视图对象的Frame属性。在学习过程中,Frame、Bounds以及Center的区别和联系是比较难以理解的,主要是需要对视图的坐标系有一定的理解,但对于初学者来说,可以记住以下要点即可。
- 绝对坐标系:屏幕的左上角是坐标原点(0, 0),横向为X轴,纵向为Y轴;向左移动X值减小,向右移动X值增加;向下移动Y值增加,向上移动Y值减小。
- 每个视图的起始位置和大小由frame属性来确定。frame是一个CGRect类型的属性,CGRect是一个结构体,其中包含有两个变量origin和size,其中origin是一个CGPoint类型的结构体变量,指的是视图左上角的那个点在绝对坐标系中的坐标,其决定了视图的位置;size是CGSize类型的结构体变量,定义了矩形的长度和宽度,从而决定了视图的大小。
- Frame:视图在其父视图坐标系中的位置和大小。建议大家在控件初始化之后,紧接着就去设置Frame,设置完成后,假如涉及修改控件的位置、大小等,就不要再去修改Frame了。
- Bounds:视图在其自身的坐标系中的位置和大小。Bounds属性中,视图的bounds.origin始终是(0,0),因此bounds属性最核心的作用是设置视图的大小,即bounds.size,当需要去修改视图大小的时候,可以修改bounds.size。
- Center:视图中心点在父视图坐标系中的坐标,当需要修改视图对象的位置时,可以修改Center属性。
图7-3 Frame与Bounds
3.示例代码
新建一个Single View Application的工程,在ViewController.m文件中添加如下代码。下面的代码通过创建3个视图展示了frame、bounds以及center这3个属性的含义,让读者深刻理解frame是视图在其父视图坐标系中的位置和大小。
运行日志如图7-4所示。
图7-4 运行日志
运行效果如图7-5所示。
图7-5 运行效果
注意:从运行效果来看,父视图的原点在屏幕的最左上角,红色视图和手机的状态栏是有重叠的,而状态栏也是被控制器管理并可以隐藏起来的。在控制器类中,实现下面的代码,即可隐藏控制栏。
运行效果如图7-6所示。
图7-6 运行效果
7.2.3 视图UIView的形变
在开发过程中,经常需要对视图对象的样式进行修改,常见的修改操作有位移、放大/缩小、旋转等。当涉及视图位移的时候,可以修改视图的center属性;当涉及视图的缩放以及旋转操作时,推荐修改视图的transform属性。
1.位移
当需要修改视图对象的位置时(上移、下移、左移、右移),可以通过修改视图对象的center属性。假如修改视图对象的frame属性,也可以实现位移的效果,但是不推荐这么做。下面的代码实现了对myView对象左移的效果,需要注意的是不能直接修改结构体变量的值,即self.myView.center.x -=10是不合法的。
2.放大/缩小
当需要修改视图对象的大小时,有两种方法可以完成。第一种方法可以修改视图对象的bounds.size属性;第二种方法是直接修改视图对象的transform属性,即让视图对象进行一次形变操作。在修改形变属性时,需要使用到CGAffineTransformScale()函数,其中:sx以及sy是在X轴以及Y轴两个方向上放大的比例。
下面的示例代码,使用CGAffineTransformScale()函数,对myView对象的transform属性进行修改,分别在X轴和Y轴方向放大1.1倍。
3.旋转
通过修改视图对象的transform属性,同样可以实现视图的顺时针旋转以及逆时针旋转,此时需要使用到CGAffineTransformRotate()函数,其中,angle参数是旋转的角度。
下面的示例代码,使用CGAffineTransformRotate()函数,对myView的transform属性进行修改,实现了视图顺时针旋转45°。
4.重置transform属性
当需要重置transform属性时,可以使用CGAffineTransformIdentity命令,但需要注意的是:如果之前对视图对象的frame、center、bounds属性进行了修改,假如需要完全重置一个视图的样式,除了重置transform属性之外,还需要重置frame、center、bounds属性,因此在对视图进行形变操作之前,有必要保存视图的原始状态以便恢复。
7.2.4 视图UIView的层次关系
在视图中可以添加子视图,通过多个视图的叠加显示,最终展示给用户。对于视图层次的管理,既可以通过InterfaceBuilder进行图形化的查看和管理,也可以通过纯代码的方式进行查看和管理,不过从实际开发经验来看,最好二选一,不要同时使用InterfaceBulider以及代码来管理子视图。
1.视图关系的常用属性和方法
在UIView的属性和方法中,提供了一些用于管理视图层次关系的操作方法,例如,可以获取一个视图的子视图或父视图,或者为一个视图添加一个子视图等。
- 有关视图层次关系的常用属性。
- 有关视图层次关系控制的常用方法。
2.通过InterfaceBuilder添加视图并设置层次关系
Xcode中的InterfaceBuilder可以提供图形化界面来添加视图类控件并且管理各个视图控件之间的层级关系。如图7-7所示,框内是一个UIView,其中包含了4个子控件,分别为1个UIImageView、1个UILabel、1个UIButton以及1个UISwitch。
图7-7 搭建界面
其中,在左侧的框内,控件的排列顺序决定了其层次优先级,可以手工调整,控件的顺序决定了控件组合后最终的显示状态。例如,手工调整UILabel以及UISwitch的顺序,可以得到如下两种展示效果,如图7-8、图7-9所示。
图7-8 手工调整子控件顺序
图7-9 改变子控件顺序
3.通过代码添加/移除子视图
除了使用InterfaceBuilder之外,还可以直接通过调用UIView的addSubview:以及removeFromSuperView方法,来添加/删除子视图。添加的子视图,会被插入subviews数组的最后,从显示效果来看,最后添加的视图显示在最前端。
下面的示例代码创建了一个addView视图对象,并且把其添加到控制器view上。
4.通过代码设置层次关系
在Interface Builder中可以通过手工方式调整控件的层次关系,同样也可以通过调用bringSubViewToFront:以及sendSubViewToBack:方法来对子视图的显示位置进行调整。当存在较多的子视图时,UIView类也提供了相对复杂的方法,来实现层级关系的精确调整,由于不太常用,此处不再赘述。
下面示例代码中,从控制器view中取出最晚添加的视图对象,然后把其移动到层次关系的最后面。
注意:在上面的示例代码中,使用了强制类型转换操作,因为从subviews中取出的最后一个对象,有可能是UIView的子类,如UIImageView、UIButton等,所以进行了类型强制转换操作。
7.2.5 视图UIView的动画
当修改UIView的某些外观属性的时候,iOS提供了动画播放的效果,能够通过动画的方式来展现出外观变化的过程,给用户提供了比较好的操作体验。
1.UIView类中支持动画的属性
动画为用户界面在不同外观状态之间的迁移过程提供了流畅的视觉效果,动画播放的功能也是苹果公司的看家本领。在UIView类中,常用的支持动画的属性如下所示,在开发过程中如果涉及修改下列属性,可以考虑添加动画效果。
2.常用动画播放的方法
当需要播放动画时,需要使用可以播放动画效果的方法。UIView类中提供的可供播放动画的方法,可以设置动画播放的时间、动画播放的内容以及动画播放完毕后的操作。另外,动画也可以支持嵌套。
在下面的示例代码中,会在1秒内修改myView的背景颜色为红色,同时放大1.2倍,当动画播放完成后,再把myView的背景颜色改为绿色并恢复初始大小。
7.2.6 响应用户交互事件
UIView类的对象都具有响应用户交互的能力,因为UIView继承自UIResponder。在初始化视图对象的过程中,可以给UIView对象添加手势,以响应用户交互。另外,对于自定义视图类,可以通过实现其有关触摸的相关方法,来定义用户交互的动作。
1.与用户交互事件相关的属性
UIView类中,userInteractionEnabled属性可以用来定义视图类对象是否能够响应用户交互。对于某些UIView的子类,例如UILabel、UIImageView,该属性默认情况下是关闭的,因此如果需要响应手势等交互事件,需要修改该属性的值为YES。另外,multipleTouchEnabled属性用于设置视图对象能否支持多点触控,默认情况下,其取值是NO。
2.添加手势
对于视图类对象,都可以通过添加手势的方法来响应用户的交互。一个视图类对象,可以添加多个手势。例如,在一些游戏App中,一个按钮的点击以及长按可以对应不同的操作。UIView类中,与手势相关的属性和方法如下:
- 获取视图对象上的所有手势。
- 添加手势。
- 移除手势。
在ViewController.m文件中添加以下示例代码,实现了为一个UIView对象添加了点击和长按手势。
- 添加myView属性并使用懒加载设置myView的属性。
- 在viewDidLoad方法中添加手势。
- 实现长按手势触发时的动作。
- 实现点击手势触发时的动作。
运行结果如图7-10所示,可以看到myView对象上添加的两个手势对象,当点击或长按myView对象管理的矩形区域时,会弹出提示框。
图7-10 运行结果
3.自定义视图类实现touches系列方法
由于UIView继承自UIResponder,因此UIView也同时具有UIResponder的属性和方法。在UIResponder类的定义中,提供了一些响应用户点击操作的方法,如下所示。在自定义视图类中,如果需要响应用户的点击操作,也可以通过重写这些方法来实现用户交互。
- 在视图管理范围内开始点击时调用。
- 在视图管理区域中移动时反复调用。
- 在视图管理范围内结束点击时调用。
需要注意视图类能够响应用户交互的范围,一定不能超出视图frame所划定的区域。大家可以做一个测试:在父视图中再添加一个更大的子视图,点击超出父视图的范围,看看是否可以调用touchesBegan:方法。
下方的示例代码可以监控用户在一个视图范围内开始触屏、滑动、结束触屏的过程。
通过运行日志可以看到,当用户开始触屏时,调用touchesBegan:withEvent:方法,当手指不离开屏幕并且在屏幕上进行滑动时,会反复调用touchesMoved:withEvent:方法,当手指离开屏幕结束触屏时,会调用touchesEnded:withEvent:方法,如图7-11所示。
图7-11 运行结果
7.2.7 内容模式contentMode
视图的contentMode属性决定了边界变化和缩放操作作用到视图上产生的效果。视图在屏幕上显示后,渲染后的内容会被缓存在视图的图层(layer)上,当视图的大小发生变化时,UIKit并不强制对视图进行重画,而是根据其contentMode属性决定如何显示缓存内容。由于这种机制的存在,当修改视图的大小时,可以提升性能。
1.contentMode简介
contentMode经常用于图像视图UIImageview。当希望在App中调整控件的尺寸时,务必优先考虑使用contentMode,这样做可以在视图的外观发生形变时,避免编写定制的描画代码。这是因为:每个图像视图的关联图像都由Core Animation来缓存,因此不需要编写代码就可以支持动画,即不需要再次调用drawRect:方法,从而大大提高了性能。
在开发过程中,当发生以下两种情况时,会使用到contentMode:
- 改变frame或bounds中的高度或宽度。
- 修改transform属性。
2.常见的contentMode
默认情况下,contentMode的值被设置为UIViewContentModelScaleToFill,意味着视图内容(一般情况下是一张图片)总是填充整个视图划定的矩形区域,有可能这张图片会被拉伸。
在实际的开发过程中,一般需要根据设计师提供的图片元素的大小来设置视图的大小,这样能够保证显示出来的图片效果不发生变形。
常见的一些contentMode对比如图7-12所示。
图7-12 常见的contentMode对比
3.contentMode的设置方法
contentMode可以在Storyboard中以及代码中进行设置。
- 使用Storyboard设置。选中一个视图控件,可以在右侧的选项中设置contentMode,如图7-13所示。
- 使用代码来设置contentMode。UIView类中提供了contentMode属性,其是一个UIViewContentMode类型的属性,默认取值为UIViewContentModeScaleToFill,UIViewContentMode是一个枚举类型,其中定义了十余种可选值,这些可选值与Storyboard中是一一对应起来的。
图7-13 设置contentMode
7.2.8 图片拉伸
UIView的某些子类,如UIImageView以及UIButton,经常会涉及图片的拉伸操作。在图片拉伸过程中,有时仅仅希望拉伸内部的部分区域,四周边缘不需要拉伸。在iOS 6之前,可以使用UIView的contentStretch属性来实现拉伸,但是从iOS 6开始,针对图片的拉伸Xcode提供了更加方便的方法。
1.图片裁剪简介
在实际的开发过程中,设计师提供的切图,有时候不一定会提供全尺寸的图片,这个时候就需要工程师对图片进行拉伸,以满足不同设备的功能需求。常见的情况是针对按钮的背景图片,设计师只会提供一个很小的图片,需要工程师在实际的开发过程中对图片拉伸。再举个例子,微信、QQ聊天的气泡,由于每次对话的文字数量不同,也需要对背景图片进行拉伸,如图7-14所示。
经过拉伸后,可以展现出各种尺寸的效果,如图7-15所示。
图7-14 微信、QQ聊天气泡
图7-15 拉伸效果
2.使用Xcode的Slicing功能
Xcode中提供了Slicing功能专门用来拉伸图片。通过Slicing功能,可以设置不需要拉伸的区域,以及需要拉伸的区域。这个功能是针对图片的设置,不需要针对UIView去做任何的操作,所有的操作均通过界面配置完成,无须编写一行代码。
- 选择Assets.xcassets,并选中需要设置拉伸的图片,如图7-16所示。
图7-16 选中拉伸的图片
- 单击右下角的Show Slicing,如图7-16所示。出现图7-17所示内容,单击图中的Start Slicing。如图7-18所示,出现3个按钮,分别对应水平方向拉伸、“水平+垂直”方向都拉伸、垂直方向拉伸。
图7-17 点击Show Slicing
图7-18 选择拉伸方向
- 一般情况下,建议选择“水平+垂直”方向都拉伸,之后出现如图7-19所示界面。图7-19中一共出现了6条分割线,一般调整上下左右4条分割线即可满足要求。
图7-19 拉伸分割线
- 调整需要保留的部分以及需要拉伸的部分,保留部分会着重显示。在调整分割线的同时,右侧的Slicing也会同时变化,如图7-20所示。
图7-20 拉伸调整
- 调整完毕后,当使用这张图片时,就会按照配置要求完成拉伸。
3.使用UIImage的resizableImageWithCapInsets:方法
除了通过Xcode的Slicing功能之外,UIImage还提供了resizableImageWithCapInsets:方法,通过该方法,也能够对图片进行拉伸操作,这个方法可以简单理解为Slicing的代码实现版。
resizableImageWithCapInsets:方法中需要传入一个UIEdgeInsets类型的参数capInsets,通过该参数可以设置在上、下、左、右四个方向需要保留的图像大小范围。
下面的示例代码中创建了一个UIImage对象,并且使用resizableImageWithCapInsets:方法设置了其在拉伸过程中上下左右切割15px的范围不参与拉伸。
在实际的开发过程中,以上方法了解即可,优先推荐使用Slicing功能。
7.2.9 使用代码创建自定义UIView
虽然苹果官方提供了一些控件,但在实际开发过程中,90%的情况下都需要根据需求去定制各种式样的UIView类。当需要定制UIView类时,可以使用纯代码以及XIB两种方式,在实现方法上它们稍有区别,但是有些共同的注意点必须特别关注,最好按照苹果建议的步骤和要求来创建自定义UIView。另外,建议在一个工程中尽量在自定义UIView以及XIB方式中二选一,这样有利于项目的整体维护。
当使用代码创建自定义UIView时,可以按照如下步骤进行。
- 新增自定义MYView类,继承自UIView类,Xcode自动生成.h/.m文件,如图7-21所示。
图7-21 新增MYView类
- 在.m文件中,实现initWithFrame:方法。在该方法中,对视图的属性进行设置,并创建、添加子视图或者添加手势对象。
- 添加子视图属性,并进行懒加载。
- 当需要对子视图进行重新布局的时候,实现layoutSubViews方法。
- 当需要对视图类的外观做自定义绘图的时候,实现drawRect:方法;下面的代码实现了为视图类四周添加一个边框。
- 如果视图需要响应用户交互,如点击等,可以为视图添加手势或者实现UIResponder类的touches系列方法。
对于自定义视图,建议程序员都采用统一的方式以及步骤来实现,在UIView不同的方法中完成不同的属性设置,这样才能够最大限度上减少不可预知问题的出现,降低代码维护的工作量。
7.2.10 使用XIB创建自定义UIView
当使用XIB创建自定义UIView时,建议大家按照如下步骤进行:
- 新增一个NibView类,继承自UIView,如图7-22所示。
图7-22 新增NibView类
- 新增一个XIB文件,命名为NibView.xib,如图7-23所示。
图7-23 新增XIB文件
- 指定该XIB文件对应的类,如图7-24所示。
图7-24 命名XIB文件
- 使用XIB绘制UIView的界面,添加子视图并设置约束关系,如图7-25所示。
图7-25 设置约束
- 由于使用XIB在初始化时不会调用initWithFrame:方法,而是调用initWithCoder:方法,因此,建议在.h文件中定义一个类方法initFromNib,并在.m文件中对该方法进行实现。该方法的主要功能从XIB文件中加载,该方法会调用initWithCoder:方法,如下代码所示。
- 假如需要对自定义UIView进行进一步的定制,定制代码也可以写在awakeFromNib方法中。
- 当需要对子视图进行重新布局的时候,实现layoutSubViews方法。
- 当需要做自定义绘图的时候,实现drawRect:方法(同代码创建自定义View)。
- 如果视图需要响应用户交互,如点击等,可以为视图添加手势或者实现UIResponder类的touches系列方法(同代码创建自定义View)。
7.2.11 控件改变坐标系(convertRect:)
由于UI控件的Frame属性都是依据该控件的父控件原点作为坐标系原点(0, 0)的,所以,当父控件不是控制器view时,无法根据UI控件的frame来获取其相对于控制器view的位置。在实际开发中,为了获取某个子控件相对于屏幕顶部、底部、左边、右边的实际距离,必须要改变子控件的坐标系。
1.坐标系转换的方法
UIView类中提供了如下四个转换方法,可以获取控件在不同坐标系的位置。
2.示例代码
如图7-26所示,图片UIImageView是灰色View的子控件,灰色View是控制器View的子控件。由于图片UIImageView的frame是参考灰色View的,因此,如果需要获取图片UIImageView相对于控制器View的位置,就必须进行坐标系的转换。
图7-26 搭建界面
为了实现示例中的要求,可以通过如下方式获取图片UIImageView相对于控制器View(self.view)的frame。
运行结果如图7-27所示。
图7-27 运行结果