iOS学习笔记(1)— UIView 渲染和内容管理
iOS中应用程序基本上都是基于MVC模式开发的。UIView就是模型-视图-控制器中的视图,在iOS终端上看到的、摸到的都是UIView。
UIView在屏幕上定义了一个矩形区域和管理区域内容的接口。在运行时,一个视图对象控制该区域的渲染;UIView继承自UIResponder,UIResponder是用来响应事件的类,UIView也具有响应事件的能力。所以说UIView具有三个基本的功能,绘制内容并管理内容的布局,响应用户交互,动画。正是因为UIView具有这些功能,它才能担当起MVC中视图层的作用。
在开发中可以使用UIKit框架中已经提供的视图组件他们大多继承自UIView,当然也可以通过继承UIView定义自己的视图。只有主线程才能更新UI。UIKit框架内的组件包括UIView及其子类的所有操作都必须在主线程中进行,否则会出现不可预知的问题。
本章主要介绍渲染和内容管理。
1、创建
- (id)initWithFrame:(CGRect)frame; 通过frame创建一个view。
2、几何属性
视图对象使用frame, bounds和center属性来跟踪它的尺寸和位置:
frame属性指定了在视图的尺寸和在父视图中的位置。
center属性指定了视图的中点在父视图的位置。
bounds属性指定了在视图本地坐标系统中视图的尺寸。默认原点是(0,0)。
可以通过center和bounds计算得到frame,反之同理。frame.origin.x == center.x - bounds.size.width / 2; frame.size == bounds.size;
transform属性用于视图的动画,通过操作transform实现视图的旋转、缩放。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
这两个方法主要用于响应者链, 用于确定视图是否为第一响应者,当用户碰触屏幕的时候系统会生成一个碰触点。为了确认哪个控件是这个碰触的第一响应者系统会调用window的hitTest方法,hitTest会调用pointInside方法判断触碰点是否在当前视图的区域内。如果在返回YES,则调用该试图所有子试图的hitTest方法;如果不在则返回NO,hitTest函数直接返回nil;如果pointInside方法返回YES,且没有子视图或者子视图的hiteTest方法返回都为nil则此视图为第一响应者,第一响应者是响应者链的开端
点转换方法。比如一个view上有一个button,在button上有一个点p,这个点相对button的坐标知道了,想知道这个点在这个view上的坐标,用这两个api去转换。
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view
[button convertPoint:p toView:view] 这样得到view上的点。
- (CGPoint)convertPoint:(CGPoint)point fromView:(UIView *)view
[view convertPoint:p fromView:button];
同点转换方法,转换一个矩形
- (CGRect)convertRect:(CGRect)rect toView:(UIView *)view
- (CGRect)convertRect:(CGRect)rect fromView:(UIView *)view
- (CGSize)sizeThatFits:(CGSize)size
返回最合适的尺寸,size是首选尺寸。只返回尺寸不会改变尺寸,如一个UILabel的text发生改变,需要UILabel的bounds调整,调用这个方法会返回一个最合适的值。
- (void)sizeToFit
按照sizeThatFits的返回值重新设置视图的bounds。
autoresizingMask 当父视图的bounds发生改变时通过这个属性决定如何调整自己的frame。缺省的值为UIViewAutoresizingNone,表示当父视图bound变化时,自己相对于父视图的frame不变。可以通过|设置多个规则,如:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
UIViewAutoresizingNone
UIViewAutoresizingFlexibleLeftMargin 到屏幕左边的距离随着父视图的宽度按比例改变
UIViewAutoresizingFlexibleWidth 视图的宽度随着父视图的宽度按比例改变
UIViewAutoresizingFlexibleRightMargin 到屏幕右边的距离随着父视图的宽度按比例改变
UIViewAutoresizingFlexibleTopMargin 到屏幕顶部的距离随着父视图的高度按比例改变
UIViewAutoresizingFlexibleHeight 视图的高度随着父视图的高度等比例改变
UIViewAutoresizingFlexibleBottomMargin
autoresizesSubviews 默认值YES,如果设置为NO那么该视图的所有直接子视图的frame自动调整行为将被忽略,也就是说无论子视图的autoresizingMask属性设置成什么都相当于置为UIViewAutoresizingNone。
3、视图层次
UIView除了提供自己的内容外,还可以作为一个视图容器。当一个视图包含其他视图时,就在两者之间建立了一个父子关系。在视觉上子视图隐藏了父视图的内容,如果一个子视图是完全不透明的,那么子视图所在区域就完全遮挡了父视图的相应区域。如果子视图是部分透明的那么两个视图在显示上就混合在了一起。父子视图关系也会影响一些视图行为,改变父视图的尺寸也会相应的改变子视图的尺寸。隐藏父视图,改变父视图的alpha值,转换父视图都会影响到子视图。
UIView通过NSArray管理子视图的。通过属性subviews可以访问视图的所有子视图。通过NSArray的特点可以得出两点:
(1)子视图的引用计数会+1,当父视图释放的时候子视图的引用计数-1。
(2)子视图是有序的,后加入的子视图会叠在上一个子视图之上。
- (void)addSubview:(UIView *)view 方法增加一个子视图。
操作子视图的方法:
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index 在指定的层级插入一个子视图。最底层是0;最顶层就是subviews count,相当于addSubview
- (void)insertSubview:(UIView *)b belowSubview:(UIView *)c b成为子视图并且在已有的子视图c的下面
- (void)insertSubview:(UIView *)b aboveSubview:(UIView *)c b成为子视图并且在已有的子视图c的上面
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2 交换两个子视图的层级。
- (void)bringSubviewToFront:(UIView *)view 视图上升到最顶层。
- (void)sendSubviewToBack:(UIView *)view 视图下降到最底层
- (void)removeFromSuperview 删除所有子视图。
superview 属性用于获取当前视图的父视图。
- (void)setNeedsLayout
调用此方法通知系统view的内容需要重新绘制,会异步调用layoutSubviews方法,view会在下一个drawing周期绘制。如果只是简单的改变view的几何形状或者当前视图并没有在窗口中显示,系统可能不会调用layoutSubviews方法。
- (void)layoutIfNeeded
调用此方法通知系统view的内容需要重新绘制,调用layoutSubviews方法。与setNeedLayout不同的时不会等到下一个drawing周期,会立即调用layoutSubviews方法。
- (void)layoutSubviews
此方法的缺省实现是空。子类可以去重写此方法当需要更精确的subviews布局。当subviews的autoresizes行为不能满足要求时才去重写此方法。可以在实现中直接设置subviews的frame。此方法不能被直接调用。如果想要在下一个drawing周期去更新view布局,应该调用setNeedsLayout方法;如果想立即更新view的布局,应该调用layoutIfNeeded方法。
layoutSubviews之所以不能被直接调用的原因可能是系统在绘制时需要进行一些操作并判断需不需要调用layoutSubviews的方法,并且在一次运行循环(run loop)中无论调用多少次setNeedsLayout都只调用一次layoutSubviews。这样就避免了资源的重复调用。
4、渲染属性
clipsToBounds 当值为YES时,子视图如果超过了当前视图的区域,超出的部分就会被裁掉。默认值是NO,也就是说当子视图显示区域大于主视图的时候还是正常显示不会裁剪。
backgroundColor 设置背景色
alpha 设置视图的透明度,当为1时不透明,为0时完全透明,既隐藏。默认值是1。alpha值会影响到子视图。如果想让父视图半透明而子视图不受影响可以设置父视图的backgroundColor = [UIColor colorWithWhite:0 alpha:0.8],这样就ok了。
opaque 给绘图系统提供一个性能优化开关。如果该值为YES,那么绘图在绘制该视图的时候把整个视图当作不透明对待。这样,绘图系统在执行绘图过程中会优化一些操作并提升系统性能;如果是设置为NO, 绘图系统将其和其他内容平等对待,不去做优化操作。为了性能方面的考量,默认被置为YES(意味着‘优化’)。
一个不透明视图需要整个边界里面的内容都是不透明的。基于这个原因,opaque设置为YES,要求对应的alpha必须为1.0。如果一个UIView实例opaque被设置为YES, 而同时它又没有完全填充它的边界(bounds),或者它包含了整个或部分的透明的内容视图,那么将会导致未知的结果。
clearsContextBeforeDrawing 决定绘制前是否清屏,默认为YES。用于提高描画性能,特别是在可滚动的视图中。当这个属性被设置为YES时,UIKIt会在调用drawRect:方法之前,把即将被该方法更新的区域填充为透明的黑色。将这个属性设置为NO可以取消相应的填充操作,view中原有内容会保留。
hidden 视图是否隐藏,默认为NO(显示),YES为隐藏。
contentMode 视图内容的填充方式。类型是UIViewContentMode。默认值是UIViewContentModeScaleToFill,填充到整个视图区域,不等比例拉伸。
typedef NS_ENUM(NSInteger, UIViewContentMode) {
UIViewContentModeScaleToFill, 填充到整个视图区域,不等比例拉伸
UIViewContentModeScaleAspectFit, 长宽等比填充视图区域,当某一个边到达视图边界的时候就不再拉伸,保证内容的长宽比是不变的同时尽可能的填充视图区域。
UIViewContentModeScaleAspectFill, 长宽等比填充视图区域,当某一个边到达视图边界的时候还继续拉伸,直到另一个方向达到视图边界。内容的长宽比不变的同时填满整个视图区域,不显示超过的部分。
UIViewContentModeRedraw, 重绘视图边界
UIViewContentModeCenter, 视图居中
UIViewContentModeTop, 视图顶部对齐
UIViewContentModeBottom, 视图底部对齐
UIViewContentModeLeft, 视图左侧对齐
UIViewContentModeRight, 视图右侧对齐
UIViewContentModeTopLeft, 视图左上角对齐
UIViewContentModeTopRight, 视图右上角对齐
UIViewContentModeBottomLeft, 视图左下角对齐
UIViewContentModeBottomRight, 视图右下角对齐
};
以上模式中凡是没有带scale的,内容bounds超出视图bounds时只有未超出部分才在视图bounds中显示。
下图很好的解释了contentMode效果
autoresizingMask 当父视图的bounds发生改变时通过这个属性决定如何调整自己的frame。缺省的值为UIViewAutoresizingNone,表示当父视图bound变化时,自己相对于父视图的frame不变。可以通过|设置多个规则,如:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
enum { UIViewAutoresizingNone = 0, UIViewAutoresizingFlexibleLeftMargin = 1 << 0, 到屏幕左边的距离随着父视图的宽度按比例改变 UIViewAutoresizingFlexibleWidth = 1 << 1, 视图的宽度随着父视图的宽度按比例改变 UIViewAutoresizingFlexibleRightMargin = 1 << 2, 到屏幕右边的距离随着父视图的宽度按比例改变 UIViewAutoresizingFlexibleTopMargin = 1 << 3, 到屏幕顶部的距离随着父视图的高度按比例改变 UIViewAutoresizingFlexibleHeight = 1 << 4, 视图的高度随着父视图的高度等比例改变 UIViewAutoresizingFlexibleBottomMargin = 1 << 5 }; typedef NSUInteger UIViewAutoresizing;
autoresizesSubviews 默认值YES,如果设置为NO那么该视图的所有直接子视图的frame自动调整行为将被忽略,也就是说无论子视图的autoresizingMask属性设置成什么都相当于置为UIViewAutoresizingNone。
contentStretch 用于制定哪部分是可拉伸的,取值在 0.0到1.0之间。下面用一个例子解释contentStretch是如何工作的。
[imageView setContentStretch:CGRectMake(150.0/300.0, 100.0/200.0, 10.0/300.0, 10.0/200.0)];
image.png的大小是 200 x 150 ;
mageView的frame是(0,0,300,200);
150.0/300.0表示x轴上,前150个像素不进行拉伸。
100.0/200.0表示y轴上,前100个像素不进行拉伸。
10.0/300.0表示x轴上150后的10个像素(151-160)进行拉伸,直到image.png铺满imageView。
10.0/200.0表示y轴上100后的10个像素(101-110)进行拉伸,直到image.png铺满imageView。
- (void)setNeedsDisPlay
调用此方法通知系统view需要重新绘制,会异步自动调用drawRect方法。
- (void)setNeedsDisplayInRect:(CGRect)rect
同样异步调用drawRect方法。在一次运行循环(run loop)中无论调用setNeedsDisplay或setNeedsDisplayInRect多少次,只调用drawRect一次。也是从减少资源开销的角度考虑的。
- (void)drawRect:(CGRect)rect
此方法的缺省实现是空。子类使用原生的绘制技术(Core Graphics and UIKit)绘制内容时应该重写此方法,在方法里面写出自己的drawing code。如果view设置自己的内容用其他的方法,则不需要去重写此方法。如果直接从UIView对象继承,实现此方法不需要call super。然而如果从其他的UIView对象继承,则应该调用super。当view 第一次显示或者当view的可视的一部分无效时,此方法会调用。此方法不能直接被调用。调用setNeedsDisplay 或者 setNeedsDisplayInRect: 方法会触发重新绘制
5、视图绘制周期
UIView类使用一个点播绘制模型来展示内容,当一个视图第一次出现在屏幕前,系统会要求它绘制自己的内容,在该流程中系统会创建一个快照,这个快照是出现在屏幕中得视图内容的可见部分。如果从来没有改变视图的内容,则这个视图绘制的方法drawRect可能永远都不会再被调用。这个快照图像在大部分涉及到视图的操作中被重用。
如果改变了视图的内容,系统也不会马上重绘视图。使用setNeedsDisPlay或setNeedsDisplayInRect方法废除该视图同时让系统在稍后重绘视图。系统等待当前运行循环(run loop)结束,然后开始重绘。这个延迟可以用来废除多个视图、增加或删除视图、隐藏、重设大小。所有的改变会在稍后一起生效。
什么是run loop?个人认为可以简单的理解为一个事件的处理过程。例如:用户点击屏幕会产生两个run loop。当用户按下的时候会产生一个run loop;当用户抬起的时候会产生另一个run loop。
如果在一个run loop中调用setNeedsDisplayInRect方法,系统会保证在这个run loop结束前调用一次drawRect方法;无论在当前run loop调用setNeedsDisplayInRect方法多少次都只调用一次drawRect。setNeedsDisplayInRect方法如果在一个run loop中刷新不同的区域,最后drawRect方法会将这些区域组合起来一起刷新,组合原则用最小的矩形圈起来所有区域,并刷新这个区域。如果刷新区域是屏幕左下角和右上角两个点,有可能刷新整个屏幕。
6、相关控件
UIWindow
UIWindow对象是所有UIView的父视图,UIWindow类是UIView的子类,可以看作是特殊的UIView。一般应用程序只有一个UIWindow对象,即使有多个UIWindow对象,也只有一个UIWindow可以接受到用户的触屏事件。UIWindow初始化在appDelegate里。 [self.window makeKeyAndVisible]方法,使window显示。
UIScreen
UIScreen用于获取设备屏幕的尺寸。
[UIScreen mainScreen].bounds 获取整个屏幕的大小;当应用程序有状态栏时,返回值:{{0, 0}, {320, 480}}
[UIScreen mainScreen].applicationFrame获取应用程序窗口的大小;当应用程序有状态栏时,返回值:{{0, 20}, {320, 460}}
UIViewController
UIViewController是MVC中的C控制器,控制管理UIView。UIViewController同UIView的关系相当于相框和相片的关系,相框可以操作相片、替换相片、决定相片的显隐藏,反之则不行。UIView工作在第一线,向用户展示表现的内容,并接受用户的交互;UIViewController负责两个方面,数据和行为,通过数据更新view,通过行为处理用户交互操作,注意这里是“处理”而不是“响应”。
UIViewController同UIView一样继承与UIResponder,所以同样处于响应者链上,同样可以接收触碰等用户事件。
可以通过下面的方法获得UIView所属的UIViewController
|