一.UIView的基本概念
UIView是视图概念的抽象,它表示屏幕上的一块矩形区域,负责渲染绘制和动画,布局和子视图管理和事件处理,所有的可视化控件都是UIView的子类。CALayer则是视图中图层概念的抽象,它负责具体的渲染绘制工作,也就是说UIView的渲染工作具体实现由CALayer来完成。
- 绘画和动画:视图使用UIKit或Core Graphics在其矩形区域中绘制内容。 视图的部分属性会自动补充过渡动画。
- 布局和子视图管理:视图可能包含零个或多个子视图。 视图可以调整子视图的大小和位置。 使用“自动布局”来定义调整视图大小和重新定位视图的规则,以响应视图层次结构中的更改。
- 事件处理:视图是UIResponder的子类,可以响应触摸和其他类型的事件。 视图可以添加手势识别器来处理常见的手势。
二.UIView的绘制和动画
绘制控制
UIView 是按需绘制的,当整个视图或者视图的一部分由于布局变化,变成可见的,系统会要求视图进行绘制。对于那些需要使用 UIKit 或者 CoreGraphics 进行自定义绘制的视图,系统会调用drawRect:
方法进行绘制。
当视图内容发生变化时,需要调用setNeedsDisplay
或者setNeedsDisplayInRect:
方法,告诉系统该重新绘制这个视图了。调用这个方法之后,系统会在下一个绘制周期更新这个视图的内容。由于系统要等到下一个绘制周期才真正进行绘制,可以一次性对多个视图调用setNeedsDisplay
,它们会同时被更新。
使用CoreGraphics API进行绘制:
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
{
// 绘图代码
}
CGContextRestoreGState(ctx);
}
iOS的mainRunloop是一个60fps的回调,也就是说每16.7ms会绘制一次屏幕,这个时间段内要完成view的缓冲区创建,view内容的绘制(如果重写了drawRect),这些CPU的工作。然后将这个缓冲区交给GPU渲染,这个过程又包括多个view的拼接compositing,纹理的渲染(Texture)等,最终显示在屏幕上。
几何属性
视图有 frame
,center
,bounds
等几个基本几何属性,其中:
frame
基于相对坐标系,其坐标位置都是相对于父视图的,可以用于确定本视图在父视图中的位置和其自身的大小。center
的坐标位置也是相对于父视图的,通常用于移动,旋转等动画操作。bounds
基于绝对坐标系,通常情况下就是(0,0,width,height), bounds 的含义可以认为是当前 view 被允许绘制的范围。
在图层平面坐标系中,使用两种坐标系。
- 基于点的坐标系:原点位于图层的左上角,向右为x轴的正方向,向下为y轴的正方向,一个点的x、y坐标以点为单位。
- 基于单位坐标系:原点位于图层的左上角,向右为x轴的正方向,向下为y轴的正方向,一个点的x、y坐标以相对x轴、y轴的比例为值,取值范围[0,1]。视图内的一个点则可以通过比例来表示,这个点称作锚点(anchorPoint),而position则是锚点在父视图中的相对坐标。
视图的ContentMode
视图在初次绘制完成后,系统会对绘制结果进行快照,之后尽可能地使用快照,避免重新绘制。如果视图的几何属性发生改变,系统会根据视图的 contentMode 来决定如何改变显示效果。
默认的 contentMode 是UIViewContentModeScaleToFill
,系统会拉伸当前的快照,使其符合新的 frame 尺寸。大部分 contentMode 都会对当前的快照进行拉伸或者移动等操作。如果需要重新绘制,可以把 contentMode 设置为UIViewContentModeRedraw
,强制视图在改变大小之类的操作时调用drawRect:
重绘。
视图的动画
设置UIView的frame
,bounds
,center
,transform
,alpha
,backgroundColor
,contentStretch
属性会自动补充上过渡动画。可使用+ animateWithDuration:delay:options:animations:completion:
来控制动画效果。
三.UIView的布局和子视图的管理
视图层级结构管理
视图可以以树形结构来组织起一个视图树,视图树里面父视图称为superView,子视图称为subView将其存储在父视图的subViews
这个属性中。subViews
这个视图容器中越晚加入的视图显示的层级就越靠近屏幕外层(层级关系直接影响到同一树中的子视图布局重叠时谁显示在外层谁显示在内层),可以通过以下的函数来进行管理操作:
addSubview
:将一个子视图添加到父视图的树中,会先调用removeFromSuperView将这个子视图移除再添加到当前的父视图中(如果已被添加的话)。insertSubview
:插入一个子视图到父视图树中,其层级将在插入位置上。bringSubviewToFront
:调整一个子视图的层级,将其显示到最上层。sendSubviewToBack
:调整一个子视图的层级,将其显示到最底层。exchangeSubviewAtIndex:withSubviewAtIndex
:交换两个视图的层级关系。removeFromSuperview
:将一个子视图从父视图树中移除。
自动布局——Autoresizing和AutoLayout
在iOS中视图的一般有两种布局方法:
- 部分手动:Autoresizing+手动Frame更新(多种屏幕尺寸得写多套代码)。
- 完全自动:Autolayout。
视图的autoresizesSubviews
属性决定了在视图大小发生变化时,如何自动调节子视图。autolayout
是IOS6以后的新技术,使用视图约束(Constraint)来自动布局。
Autoresizing
当UIView
的autoresizesSubviews
是YES
时(默认是YES),那么在其中的子view会根据它自身的autoresizingMask
属性来自动适应其与superView
之间的位置和大小。autoresizingMask
是一个枚举类型, 默认是UIViewAutoresizingNone
, 也就是不会autoresize。
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0, //view的frame不会随superview的改变而改变
UIViewAutoresizingFlexibleLeftMargin = 1 << 0, //自动调整view与superview左边的距离保证右边距离不变
UIViewAutoresizingFlexibleWidth = 1 << 1, //自动调整view的宽,保证与superView的左右边距不变
UIViewAutoresizingFlexibleRightMargin = 1 << 2, //自动调整view与superview右边的距离保证左边距不变
UIViewAutoresizingFlexibleTopMargin = 1 << 3, //自动调整view与superview顶部的距离保证底部距离不变
UIViewAutoresizingFlexibleHeight = 1 << 4, //动调整view的高,保证与superView的顶部和底部距离不变
UIViewAutoresizingFlexibleBottomMargin = 1 << 5 //自动调整view与superview底部部的距离保证顶部距离不变
};
这个属性使用的是一个枚举类型,即意味着它可是进行组合使用。
AutoLayout
AutoLayout是为了解决不同尺寸屏幕的适配问题而实现的技术,它通过为视图添加约束条件(Constraint,可以想象为一个不可形变的撑杆,而视图是一种可形变的物体,当给视图添加约束后视图则会根据运动情况自动形变)来实现视图的布局自动调整。
attribute1 == multiplier × attribute2 + constant
其中方程两边不一定是等于关系,也可以是大于等于之类的关系。基于Constraint实现的Autolayout比AutoResizing 更加灵活和强大,可以实现复杂的子视图布局。
原生的Autolayout的语法较为复杂,而现在出现了一些基于Autolayout实现的第三方库如Masonry简化了Autolayout的语法。
手动布局
UIView 当中提供了一个layoutSubviews
函数,UIView 的子类可以重载这个函数,以实现更加复杂和精细的子 View 布局(主要设置视图的Frame
属性)。
苹果文档专门强调了,应该只在上面提到的 Autoresizing 和 Constraint 机制不能实现所需要的效果时,才使用layoutSubviews
。而且,layoutSubviews
方法只能被系统触发调用,程序员不能手动直接调用该方法但可以间接通过setNeedsLayout
标记一个视图需要进行更新,使用layoutIfNeeded
触发layoutSubviews
。
- (void)layoutSubviews//在这个函数中进行子视图的布局更新
- (void)layoutIfNeeded//如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)
- (void)setNeedsLayout//标记视图需要布局更新,异步调用layoutIfNeeded刷新布局,不立即刷新。
layoutSubviews
方法具体调用的时机有下面几种情况:
- 在父 view 的 autoresize mask 为 ON 的情况下,addSubview 会导致被 add 的 view 调用
layoutSubviews
, 同时 add 的 target view 以及它所有的子 view 都会被调用。 - setFrame 当新的 frame 和 旧的不同时(即 view 的大小改变时)会调用
layoutSubviews
。 - 滚动一个 UIScollView 会导致这个 scrollView 以及它的父 View 调用
layoutSubviews
。 - 旋转设备会导致当前所响应的 ViewController 的主 View 调用
layoutSubviews
。 - 改变 View 的 size 会导致父 View 调用
layoutSubviews
。 removeFromSuperview
也会导致父 View 调用layoutSubviews
。
四.UIView的事件处理
UIView 是 UIResponder 的子类,可以响应触控事件。
通常可以使用addGestureRecognizer:
添加手势识别器来响应触控事件,如果需要手动处理,则按需要重载 UIView 中的下面四个函数:
touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent: