UIView:这些函数你清楚吗
开头说几句
UIView表示屏幕上的一块矩形区域,App上看得见,摸得着的都是UIView或者其子类对象。其主要功能有:
- Drawing and animation(绘图与动画)
- Layout and subview management(布局与子视图的管理)
- Event handling(事件处理)
这篇文章主要会讲一下UIView的一些细节方面,对基础知识不做探究了。
老生常谈,先来看看这些常见的函数。如果看过类似的,不要走,跳过这一节。
关于绘图与布局
setNeedsDisplay
将视图标记为需要重绘,并异步调用drawRect:
setNeedsDisplayInRect:(CGRect)rect
将视图标记为需要局部重绘
drawRect:(CGRect)rect
重写此方法,执行重绘任务
drawRect被调用情况:
-
ViewController -> loadView -> viewDidLoad
之后(但当UIView在初始化时未设置rect大小,将导致drawRect
不被自动调用) - 调用了
sizeToFit
之后 - 设置了
view.contentMode = UIViewContentModeRedraw
,每次设置或更改frame之后(不推荐使用) - 调用了
setNeedsDisplay
ORsetNeedsDisplayInRect:
之后,(但rect ≠ 0)
Tips:
- 不可显式调用,只能通过setNeedsDisplay或者setNeedsDisplayInRect系统自动调用
- 使用UIView绘图,只能在方法内;使用CALayer绘图,只能在drawInContext:中,或在delegate的相应方法
drawRect可以提供给我们在自定义界面时的方便,但也要慎用。
具体可以看看这篇链接: 内存恶鬼drawRect
layoutSubviews
布局子视图。默认没有执行什么过程,需要子类重写。
layoutSubviews被调用情况:
- 调用了
initWithFrame:
,且rect ≠ CGRectZero时 - 调用了
addSubview
- 设置了frame,且oldFrame ≠ newFrame
- Screen发生变化,比如UIScrollView滚动,屏幕旋转,来电等
Tips:若想在外部设置subviews位置时,则不要重写它
setNeedsLayout
标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新。且layoutSubviews一定会被调用
layoutIfNeed
如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)
sizeThatFits:
让视图计算最适合子视图的大小,即能把全部子视图显示出来所需要的最小的size
Tips:sizeThatFits传入的参数是接收者当前的size,返回一个适合的size
sizeToFit
根据子视图的大小位置,调整视图,使其恰好围绕子视图。也就是说自动适应子视图的大小,只显示子视图
这个方法经常用于UILabel一类控件,使用sizeToFit能使得控件根据文字来调整尺寸。(或者不方便手动调整尺寸的地方,譬如很多系统的原生控件我们无法改变大小,估计就是据此原理)
Tips:
- 不应该在子类被重写,只能重写sizeThatFits:
- sizeToFit会调用sizeThatFits:
- sizeToFit跟sizeThatFits都无传递链,只对自身负责
小结
类似的还有约束处理的这几个函数
- (BOOL)needsUpdateConstraints //视图的约束是否需要更新
- (void)setNeedsUpdateConstraints //设置视图的约束需要更新
- (void)updateConstraints //为视图更新约束
- (void)updateConstraintsIfNeeded //更新视图和其子视图的约束
细致地了解这些关键函数,根据iOS的显示刷新原理我们能更好简便地去使用它们。譬如界面约束更改的动态展示,以及view线条的动态绘制等等。
关于展示与动画
CALayer
为什么要扯到CALayer呢?其实UIView之所以有显示能力,主要是来源于自身具备着的CALayer。CALayer基于图像管理内容并允许你在这些内容上创建动画。一言以蔽之,UIView来自CALayer,高于CALayer,是CALayer高层实现与封装;UIView的很多特性都源于CALayer对它的支持。
把UIView看成一张相片,那么CALayer就是相框。相框有圆形等各种形状,有边框粗细,有边框颜色等等,所以依靠定制CALayer,则能实现各式各样的UIView。UIView有各式各样的动态效果,也是依赖于CALayer的变换。CALayer的我想再起一文来讲,所以这里就暂且一笔带过了。
关于事件
iOS系统在处理事件时,通过UIApplication对象和每个UIWindow对象的sendEvent:方法将事件分发给具体处理此事件的responder对象(对于触摸事件为hit-test view,其他事件为first responder),当具体处理此事件的responder不处理此事件时,可以通过responder chain交给上一级处理。
事件的传递链
如下图所示,iOS中事件传递首先从UIApplication开始,接着传递到UIWindow,再到UIView、Subview。传递顺序从下到上。
PS: 如果是VC注册了手势操作,在UIWindow接着往下传递到View之前,Window会将事件交给GestureRecognizer,如果在此期间,GestureRecognizer识别了传递过来的事件,则该事件将不会继续传递到下一级去,而是交给Target(ViewController)进行处理。以此类推。
iOS事件传递事件的响应链
事件的响应顺序跟传递顺序是相反的。响应顺序是从上到下。
接下来请带着一个问题来看下面的知识点:当你点击了一下屏幕时那一瞬间整个应用发生了什么事情?
touchesBegan../Moved../Ended..
应用找到合适的视图时,就会调用视图控件的touches系列方法进行处理。
Tips:在某个视图实现了此方法后,会拦截事件的响应。父视图一类无法监听到触摸事件。
pointInside:withEvent
判断是否点在视图上.
Tips: 如果我们不想让某个视图响应事件,也可以重载pointInside:withEvent:方法,让此方法返回NO
hitTest: withEvent:
hit-Testing的作用就是找出这个触摸点下面的View是什么,HitTest会检测这个点击的点是不是发生在这个View上,如果是的话,就会去遍历这个View的subviews,直到找到最小的能够处理事件的view,如果整了一圈没找到能够处理的view,则返回自身。
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
//recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
Tips:
- hitTest时会调用pointInside:withEvent:
- 若出现以下情况时,hitTest会直接返回nil而不会调用pointInside:
- view.userInteractionEnabled = NO;
- view.enabled = NO(UIControl);
- view.alpha <= 0.01;
- view.hidden = YES;
- 默认的hit-testing顺序是按照UIView中Subviews的逆顺序
- 如果同级别的Subview中有重叠的部分,则优先检查顶部的Subview,如果顶部的Subview返回nil,再检查底部的Subview
- 若点击事件没有发生在视图A上,是会忽略视图A的subview,即使subview.clipsToBounds = NO且超出A的边界。
每当我们点击了一下iOS设备的屏幕,UIKit就会生成一个事件对象UIEvent,然后会把这个Event分发给当前active的app。
官方原文 Then it places the event object in the active app’s event queue.
告知当前活动的app有事件之后,UIApplication 单例就会从事件队列中去取最新的事件,然后分发给能够处理该事件的对象。UIApplication 获取到Event之后,Application就纠结于到底要把这个事件传递给谁,这时候就要依靠HitTest来决定了, histTest返回结果不为nil的时候则默认不再向父视图传递。
小结
讲了这些有什么用呢?很多时候不规则按钮一样的定制,比如一个圆形按钮,屏蔽圆形按钮四个角的触摸事件怎么去处理?在滚动视图的优化上,类似微博,twitter这种每个cell可能承载大量子视图,而且又会有很多数据时,大量的UIView是会占用性能的,使用CALayer+hitTest就可以省略了很多性能消耗。
最后讲两句
这里只是讲些浅显的东西,只不过我们在用每个函数时,准确理解其意义跟原理有助于我们让代码更简洁有效。
关于iOS的界面性能优化上,本来也想讲讲,不过大牛ibireme这篇文章 iOS 保持界面流畅的技巧 已经写得很全面详细了,个人简直拜服,这里就直接推荐出来了。