View
iOSAPP中和用户打交道最多就是view,view有多作用,下面随便列举几个:
- 布局管理
- a view能够定义和父视图相关的,默认的大小变动行为
- a view使用一个数组管理它的subviews
- a view能够改变subview的size和position
- a view能将一个坐标系下的point转换到另一个坐标系的点
- 绘制内容和动画
- 能够在一个矩形区域绘制内容
- view的属性可以做动画
- 响应事件
- view可以接受事件
- view参与响应链
本文讲解如何创建view,管理views,绘制内容,view的层级树,view如何处理事件传递(更多内容请看Event Handling Guide for iOS)
创建和设置View
可以使用代码手动创建也可用XIB创建,创建完view后,将其组合到view的层级树中.
使用XIB创建View
- xib创建view是一种便捷方式,在xib中你可以拖拽UI元素进入你的界面,配置各种属性. xib的另一个好处是,所见即所得(xib中见到和运行时的一样)方便调试. 你可以将view的行为和代码绑定起来,这样view可以进行用户交互. 创建好后,xib会将view和view状态等配置信息保存在
nib
文件中(一种资源文件) - 通常一个nib文件代表一个整个view层级树,顶层是controller的view,然后再往controller的view中添加其他view. 要注意顶层view的大小要和设备以及内容匹配.
- 通常一个nib文件是和viewController绑定在一起的,在使用是controller会自动从nib中加载UI界面; 如果你的nib文件没有和controller绑定在一起的话,可以使用
NSBundle
或者UINib
来手动从nib文件中加载界面.
想要学习更多的关于xib使用的知识请参考Apple文档Interface Builder User Guide
以及controller如何加载nib文件,创建自定义viewController请看View Controller Programming Guide for iOS
以及学习如何手动从nib文件中加载UI界面的知识请看Resource Programming Guide中的Nib Files
使用代码创建View
通常使用allocation/initialization模式来创建view的,view的默认初始化方法是initWithFrame:
CGRect viewRect = CGRectMake(0, 0, 100, 100);
UIView* myView = [[UIView alloc] initWithFrame:viewRect];
注意:虽然所有的view都支持
initWithFrame:
方法,但有的view有其自己的初始化方法,比如UIButton,通常都是使用buttonWithType:
来创建的,UIImageView的initWithImage:
等等.
view创建后需要将其添加到window中或其他view中,否则不能显示.
给view的属性赋值
通过UIView的属性来控制view的显示和行为.
下表展示view的属性和作用
Properties | Usage |
---|---|
alpha, hidden, opaque | 这些控制view透明度. 注意opaque 属性,opaque属性设置为YES可以提高性能 |
bounds,frame,center,transform | 这些属性控制view的size和position. transform 用来做动画或者做view的复杂整体移动 |
autoresizingMask, autoresizesSubviews | 这些属性用来控制view和subviews的automatic resizing行为. 当superview的bounds发生改变时,autoresizingMask 控制view的变化;autoresizesSubviews 控制view的subviews是否需要resize. |
contentMode,contentStretch,contentScaleFactor | 这些影响view的内容绘制. contentScaleFactor 属性用于需要自定义重绘view的高分辨率的屏幕. |
gestureRecognizer, userInteractionEnabled, MultipleTouchEnabled, exclusiveTouch | 这些属性控制view对于touch events的处理. |
backgroundColor, subviews, drawRect:,layer | 这些属性控制view的内容显示和绘制 |
想要知道更多请看UIView的接口UIView Class Reference
给view添加一个记号
UIView有个tag
属性(integer,整型,默认为0),用来标记view的,方面后续使用tag值从view的层级树中找到该view.使用tag来获取view比遍历寻找要快.
通过UIView
的实例方法viewWithTag:
,该方法使用深度优先算法(参考数据结构-树)从层级树中搜索目标,而且只会从view的本身和subview开始搜索,view的superview和其他层级树不会搜索,也就是说如果你对root view调该方法,那么它会搜索整个页面的层级树,如果是树中的某个view调用该方法,那么只会搜索某个子树.
创建和管理view的层级树
创建和管理view的层级树,就是创建和管理APP的UI界面,层级树决定了那个view响应事件. 下图展示Clock应用的图层,由许多view构成UI界面:
这一节讲解如何创建view的层级树,以及如何从层级树找到特定的view,转换不同的view的坐标系.
添加移除subview
如果使用xib创建view层级树,那么可以直观地发现view之间的层级(父-子关系),而且界面不需要运行就可以看到.
使用代码创建的话,需要使用下么方法来创建和管理:
- 将subview添加到superview使用
addSubview:
方法,该方法将subview添加superview的属性subviews
数组中末尾 - 要将subview加入superview的subviews中的某一个为使用方法
insertSubView:...
- 要想将某个view位置改变一下,可以使用
bringSubviewToFront:
,sendSubviewToBack:
,exchangeSubviewAtIndex:withSubviewAtIndex:
,使用这些方法比使用add,remove,insert等方法要快. - 想将一个view从superview中移除,可以使用
removeFromSuperview
方法
当一个subview添加到superview后,会根据frame来确定位置和大小. subview超出superview的区域默认是可见的,如果你想superview裁剪subview,可以将superview的clipsToBounds
设置为YES.
往view的层级树中插入subview的代码可以写controller的loadView
(适合手动用代码)或者viewDidLoad
中(适合xib)
下列代码展示了Apple官方demoUIKit Catalog (iOS): Creating and Customizing UIKit Controls中类TransitionsViewController
方法viewDidload
中的代码. TransitionsViewController
用来管理两个view间切换的动画. viewdidload中的代码顺序地创建一个容器view,image views用来做切换动画. 容器view的作用是方面做两个image间的切换动画.
- (void)viewDidLoad {
[super viewDidLoad];
self.title = NSLocalizedString(@"TransitionsTitle", @"");
// create the container view which we will use for transition animation (centered horizontally)
CGRect frame = CGRectMake(round((self.view.bounds.size.width - kImageWidth) / 2.0),
kTopPlacement, kImageWidth, kImageHeight);
self.containerView = [[UIView alloc] initWithFrame:frame];
[self.view addSubview:self.containerView];
// create the initial image view
frame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight);
self.mainView = [[[UIImageView alloc] initWithFrame:frame] autorelease];
self.mainView.image = [UIImage imageNamed:@"scene1.jpg"];
[self.containerView addSubview:self.mainView];
// create the alternate image view (to transition between)
CGRect imageFrame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight);
self.flipToView = [[[UIImageView alloc] initWithFrame:imageFrame] autorelease];
self.flipToView.image = [UIImage imageNamed:@"scene2.jpg"];
}
如果你将一个subview又添加到另一个view,UIKit会通知它superview和它的subview. 如果是自定义的view,你可以在重写下面方法来监听该通知:
-
willMoveToSuperview:
,willMoveToWindow:
,willRemoveSubview:
-
didAddSubview:
,didMoveToSuperview
,didMoveToWindow
你可以使用上述通知来做一些和view层级树变动有关的操作
隐藏view
- 有两种方式:①设置属性
hidden
为YES②设置属性alpha
为0.0 - 隐藏的view不会响应事件,但会参与view的布局
- 如果想移除一个view通常隐藏该view,特别是当该view在未来某刻需要显示
- 如果想给view做个隐藏/显示动画,那么你应该使用
alpha
而不是hidden
注意:如果你隐藏的view当前是first responder,那么事件会继续传递给它,所以你因该在隐藏它同时将其resign first responder. 更多关于响应链的知识请看Event Handling Guide for iOS
如何在层级树中找到特定的view
- 有两种方法:①通过保存一个该view的一个引用 ②设定一个唯一性的tag值,在使用
viewWithTag:
寻找 - 通过引用方法的经常使用,但使用tag的方法更加灵活硬编码少点.而且tag的方式也可以用来做数据的持久化操作,界面的恢复.比如,在做界面恢复操作时,可以先用个文件保存view的tag,然后将该文件写到磁盘中,比把正界面保存好多了.在界面恢复时,根据tag值可以快速确定view间的关系和是否需要显示.
view的位移/缩放/旋转
- 每个view都有个
transform
属性用来给view做仿射变换的, 改变view的transform会影响view的最终渲染的结果,一般用于实现滚动,动画,等视觉效果. - view的属性
transform
的类型是一个CGAffineTransform
结构体,默认值是identity transform(不会改变view外观).你可以随时给transform赋值,如下:
// M_PI/4.0 is one quarter of a half circle, or 45 degrees.
CGAffineTransform xform = CGAffineTransformMakeRotation(M_PI/4.0);
self.view.transform = xform;
下图展示了transform如何旋转一张图片:
- 给view添加的多个仿射变换的顺序会影响最终结果,比如选择后位移和位移后选择的结果是不一样的,即使旋转和位移的次数相同.做放射变换时view的center是不会变的,想知道更多的知识请看文档Quartz 2D Programming Guide中的Transforms
切换不同的坐标系
很多时候,特别是在处理touch events的时候,经常要计算一个view的坐标在其他view中的坐标; 比如要计算touches在某个view中的坐标. UIView
提供了下面的方法用来计算其他view在该view本地坐标:
convertPoint:fromView:
convertRect:fromView:
convertPoint:toView:
convertRect:toView:
上面方法中convert...:fromView:
将其他view中的坐标转换到当前view的坐标,相反地,convert...:toView:
试讲当前view的坐标转换到其他view中的坐标.在上面4个方法中如果view的值设为nil,那么自动地认为和window进行转换.
UIWindow
也停供了和UIView
类似的工具方法:
- convertPoint:fromWindow:
- convertRect:fromWindow:
- convertPoint:toWindow:
- convertRect:toWindow:
这里有个涉及将一个旋转过的view中的坐标转换到其他view的问题,UIKit会算出该旋转view刚好包含旋转view的矩形框,然后再讲矩形框转换到其他view的坐标,看下图解释:
如何在运行时调整view的大小和位置
只要view的size改变了,那么view的subview的position和size也要相应的改变. UIView提供两种方式进行View的布局:①自动布局(当superview变动时,设置view间的布局规则,实际的位置和大小有系统根据前面设置的规则自己计算) ②手动布局(superview的size改变时,开发者自己计算subview的size和position)
为布局变动做准备
布局的变动会因为下面的这些原因:
- 改变view中bounds的size
- 旋转了界面方向,通常会改变root view的bounds
- view的layer中加了CoreAnimation要求改变布局
- 调用了view的
setNeedsLayout
和layoutIfNeeded
方法 - 给view的layer发送
setNeedsLayout
消息
使用Autoresizing(和autolayout不一样)进行布局
- 当view的size改变时,view可以用属性
autoresizesSubviews
来控制subviews是否要重新resize. 如果给整个属性设置为NO,那么当view改变时它的subview也不会重新布局. - subview使用
autoresizingMask
来决定subview如何进行大小和位置的设置. - 同样的规则对subview的subview同样有效.
在自动布局的时候,给view设置autoresizingMask
很重要,下表列举了autoresizingMask(宽高上下左右)可能的取值,和每一个值对应的布局操作,并且这些值可以叠加(做或运算),然后赋值给view的autoresizingMask
. 如果你是XIB来矩形局部可以使用Autosizing inspector进行相应的设置.
Autoresizing Mask | 描述 |
---|---|
UIViewAutoresizingNone | 不进行autoresize(默认值) |
UIViewAutoresizingFlexibleHeight | 高度随superview而变,如果不包含该值,高度不会改变 |
UIViewAutoresizingFlexibleWidth | 宽度随superview而变,如果不包含该值,宽度不变 |
UIViewAutoresizingFlexibleLeftMargin | view的左边和superview左边的距离可以可变,如果不包含该值,那么间距不变 |
UIViewAutoresizingFlexibleRightMargin | view的右边和superview右边的距离可以可变,如果不包含该值,那么间距不变 |
UIViewAutoresizingFlexibleBottomMargin | view的底边和superview底边的距离可以可变,如果不包含该值,那么间距不变 |
UIViewAutoresizingFlexibleTopMargin | view的顶边和superview顶边的距离可以可变,如果不包含该值,那么间距不变 |
下图展示上面取值代表物理意义上的图示,某一个值的缺失代表这一物理意义是固定值,否则是随superview的大小可变. 如果你对view进行配置是,在同一轴上有多个可变配置,比如你对一个view同时设置UIViewAutoresizingFlexibleTopMargin和UIViewAutoresizingFlexibleBottomMargin,那么UIKit会这一轴上平均的分配任意大小
上面的配置同xib中的Autoresizing inspector来设置autoresizingMask最简单,而且还有一个动画展示方便理解.
注意:如果view的
transform
的值不为identity transform,那么view的frame会失效,同样地对autoresizingMask也是一样.
当对view进行了autoresizing设置好,UIKit还有提供一个接口开发者手动的调整view的布局.
手动对view的布局进行调整
当一个view的size改变时,UIKit利用autoresizingMask对view的subview进行autoresizing,然后调用view的layoutSubViews
方法,以供开发者手动调整.你可以在自定义view中重写该方法:
- 调整subview的size和position
- 添加或者移除subview或者CoreAnimation layer
- 给subview发送
setNeedsDisplay
或setNeedsDisplayInRect:
消息强制subview重绘
特别提醒:如果你的应用中有个需要滚动显示大量视图的view,那么layoutSubviews
方法中的代码很重要. 因为用一大块显示所有的内容是不现实的,通常的做法是将大量内容分块显示在subview中,就像砖头(tile View)一样,可以复用. 所以view滚动时,在layoutSubViews
中需要将显示完的tile View的位置放到即将要显示的位置,然后重绘它的内容. 关于如何显示tileview的具体做法可以参考Apple的demoScrollViewSuite
当你进行布局时,代码中要确认下面几件事:
- 当旋转手机屏幕时,你的布局代码是否还能正确生效
- 你的布局代码能否适应status bar高度的改变,因为status bar有时会变,比如电话进来后status bar的高会变化.
想学更多关于autoresizing的知识请参考苹果文档Handling Layout Changes Automatically Using Autoresizing Rules
在运行时修改view
view会因为用户事件改变(size,position,hidden,或者创建一个view的层级树等),在iOS中view的改变可以发生下面的位置或者一下面的方法就行改变:
- 在view controller中
- view controller负责创建界面需要的view,可以从nib文件中加载,也可以从代码中创建,而且controller也负责干掉无用了的view
- 当屏幕旋转时,controller负责调整view(大小位置隐藏创建等改变)
- 当controller处理可编辑内容时,在进入/退出可编辑状态时,controller可能会调整view的层级树; 比如,添加一个额外的button和其他控件来处理编辑内容,这需要调整view的层级树.
- 在Animation block中
- 你可能会在Animation block处理两组view的切换,隐藏界面中的一组view然后显示另一组view
- 当你需要实现一个特殊的动画时,你在Animation block中会对view的属性进行各种调整; 比如改变一个view的size
- 其他方式
- 你可能创建一组新的view以响应手势或者其它用户事件
- 当你滚动scroll view时,你可能会同时隐藏和显示tile subview
- 当键盘显示时,你可能会reposition和resize被键盘遮住的部分view,关于更多和键盘交互的知识请看Text Programming Guide for iOS
view controller是view的层级树的管理者,大部分的view的修改都发生在这里,controller是view改变的终极负责人. 特别地,你可以在view controller中的setEditing:animated:
方法中,将用户界面切换到可编辑模式.
Animation block中是另一个频繁需要修改view的地方. UIView内置的动画接口可以做一些简单的动画,比如你可以用如下几个方法进行view的切换动画:
transitionWithView:duration:options:animations:completion:
transitionFromView:toView:duration:options:completion:
CoreAnimation Layers的交互
每个view都一个layer用来展示内容和动画. 尽管你通过view可以做很多,但你也可以直接操作view的layer
修改view的layer class
view中的layer类型在view创建后是不能修改的,因此可以通过view的layerClass
类方法修改layer的类型.这个方法的默认实现是返回[CALayer class]
,你可以在自定义view中重写该方法然后返回想要的layer类型,如下代码返回CATiledLayer
类型.
+ (Class)layerClass {
return [CATiledLayer class];
}
每个view在初始化实例之前会调用上面的方法返回layer的类型,然后根据类型创建layer对象. 另外将view自己设置为layer的delegate,此时layer和view的联系就建立起来了,之后不能改变,你不能再将view自己设置为别的layer的delegate,如果你修改layer和view之间的关系,会导致view的内容绘制出问题,和其他一些不可预的问题(比如crash掉)
知道其他Layer类型和作用吗?请看Core Animation Reference Collection
往view中插入其他layer对象
如果你偏向使用layer而不是view,那么你可以将一个自定义的layer插入到view中. 一个自定义的layer对象是一个没有任何view绑定的CALayer实例. 自定义layer中要使用Core Animation代码,layer无法响应事件只能绘制内容,可以响应view的size变化
下面的代码展示了,如何使用layer,该layer用来显示一个图像:
- (void)viewDidLoad {
[super viewDidLoad];
// Create the layer.
CALayer* myLayer = [[CALayer alloc] init];
// Set the contents of the layer to a fixed image. And set
// the size of the layer to match the image size.
UIImage layerContents = [[UIImage imageNamed:@"myImage"] retain];
CGSize imageSize = layerContents.size;
myLayer.bounds = CGRectMake(0, 0, imageSize.width, imageSize.height);
myLayer = layerContents.CGImage;
// Add the layer to the view.
CALayer* viewLayer = self.view.layer;
[viewLayer addSublayer:myLayer];
// Center the layer in the view.
CGRect viewBounds = backingView.bounds;
myLayer.position = CGPointMake(CGRectGetMidX(viewBounds), CGRectGetMidY(viewBounds));
}
你可以往view中加入多个layer,因为view的layer也有个数组属性sublayers
来保存加入view中的layer, 具体请看Core Animation Programming Guide
如何自定义view
当UIKit提供的view无法满足需求时,就必须走上自定义view的道路. 自定义view可以完全由你控制,非常灵活.
注意:如果你使用OpenGL ES绘制内容的话,你必须使用GLKView代替继承
UIView
.具体请看OpenGL ES Programming Guide
关于实现自定义View的基本操作
实现自定义view要做的事主要有两件:①展示内容 ②管理view的交互,当想更好的实现自定义view光这两点还不够,下面列举了实现自定义view需要完成的步骤:
- 给view定义几个何时的初始化方法:
- 如果手动创建,需要重写
initWithFrame:
方法,或者自定义一个初始化方法 - 如果重nib文件中加载,重写
initWithCoder:
方法,在该方法中对view进行一些状态设置
- 如果手动创建,需要重写
- 显示dealloc方法,用来销毁一些对象的
- 要想定制任何内容就需要重写
drawRect:
方法: - 设置属性
autoresizingMask
给view加上autoresizing功能 - 如果你的view需要集成和管理许多的subview,那么:
- 在初始化view的时候,创建subviews
- 在创建subview的时候顺便设置各个subview的
autoresizingMask
属性 - 如果view的subview需要手动布局,重写view的
layoutSubviews
- 实现touch-event,那么:
- 通过
addGestureRecognizer:
方法给view添加合适的手势 - 如果你想手动处理touches,那么可以重写view的
touchesBegan:withEvent:
,touchesMoved:withEvent:
,touchesEnded:withEvent:
,touchesCancelled:withEvent:
四个方法(不管其他touch方法有没有重写,牢记你需要始终重写touchesCancelled:withEvent:
方法)
- 通过
- 如果你想定制打印的view,那么你需要重写
drawRect:forViewPrintFormatter:
方法,具体请看Drawing and Printing Guide for iOS
另外,在重写上面提到的方法中,你可以对view的许多属性进行操作,比如contentMode
,也可以直接地或间接地的操作layer
初始化自定义view
每个自定义的view都需要提供initWithFrame:
初始化方法.该方法在你手动创建的view初始化时调用.下面的代码展示了一个initWithFrame:
方法的模板,在重写该方法时,你需要调用父类的的方法,设置view的状态,初始化实例变量,然后再将初始化完成的view返回.
- (id)initWithFrame:(CGRect)aRect {
self = [super initWithFrame:aRect];
if (self) {
// setup the initial properties of the view
...
}
return self;
}
如果从nib文件中加载view,那么你要记得回调用initWithCoder:
方法而不是initWithFrame:
,该方法是协议NSCoding的一部分. 在该方法中,你可以view的状态进行设置,也可以重写awakeFromNib
方法对view进一步设置.
实现重绘
如果自定义view需要绘制内容,那么需要重写drawRect:
方法,在刚方法中实现重绘. Apple建议如果不是迫不得已的话,最好还是不要走重绘的路,可以用其他view代替.
在drawRect:
方法中只能干和内容绘制相关的内容,像更APP的数据结构等其他和绘制无关的操作千万不要放到这个方法中.该方法中的任务要尽量快速完成,如果你频繁调该方法的话,那么需要优化你的绘制算法,能够快速完成.
在调用drawRect:
方法前,UIKit会先给view配置内容绘制环境. 特别是创建graphic context对象和调整坐标系. 当环境创建后,你才能用UIKit和core graphic等技术进行绘制.可以通过UIGraphicsGetCurrentContext
方法来获取当前绘画上下文.
注意:当前绘画上下文(current graphics context)只要在调用
drawRect:
时有效. UIKit可能会在不同绘制操作步骤中创建不同的绘画上下文,所以你不要将该对像缓存起来供未来使用.
下面代码展示了使用drawRect:
方法绘制一个边宽为10.0的view:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect myFrame = self.bounds;
// Set the line width to 10 and inset the rectangle by
// 5 pixels on all sides to compensate for the wider line.
CGContextSetLineWidth(context, 10);
CGRectInset(myFrame, 5, 5);
[[UIColor redColor] set];
UIRectFrame(myFrame);
}
如果你知道你的view的内容是不透明的,那么你可以将view的opaque
属性设置为YES,这样可以提高性能. 如果你设置NO的话,UIKit还要绘制被view遮住的内容.
另外一个提高view性能的操作是设置clearsContextBeforeDrawing
为NO,特别地,当滚动view的时候.如果你设置为YES的话,在drawRect方法更新内容之前,UIKit要自动地将view设置透明黑色. 设置NO可以避免这一操作.
响应事件
view是一个响应者(因为UIView集成UIResponder). 为了能够直接响应事件,view可以通过手势监听像taps,swipes,pinches等等这些手势,但这是Apple封装好的,你要可以重写view的touches方法来自定义响应事件:
- touchesBegan:withEvent:
- touchesMoved:withEvent:
- touchesEnded:withEvent:
- touchesCancelled:withEvent:
如果你想开启多手指事件设置multipleTouchEnable
为YES.
有的view,比如label是默认关闭监听用户事件的,既可以设置userInteractionEnabled
为YES
你可以通过UIApplication
对象的beginIgnoringInteractionEvents
和endIgnoringInteractionEvents
方法来控制整个APP的事件响应能力
注意:用UIView提供的动画方法进行动画时是无法响应用户事件的. 你可以通过重写相应方法来配置相应的特性,具体细节请看本系列文章(四)
在事件传递过程中,可以通过hitTest:withEvent:
和pointInside:withEvent:
方法判断一个view是否具有响应特定event的能力.
垃圾清理-dealloc
自定义view有时需要用到该方法来清理垃圾. 不过很少用.