-
原理部分
-
UIView与CALayer
iOS中所有的UI控件都有一个共有父类UIView,而提起UIView,始终有一个CALayer对象伴随。那么UIView与CALayer之间关系就是研究UI的一个关键。
先说结论:CALyaer是UIView的属性,UIView是CALayer的代理。
如何理解这句话呢,我们先看两个类UIResponder和CALayer
UIResponder类是专门用来响应用户的操作处理各种事件的,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events,如插入耳机调节音量触发的事件)。
CALayer包含在
QuartzCore
框架中,这是一个跨平台
的框架,既可以用在iOS中又可以用在Mac OS X中,用来绘制内容与处理各种动画。我们知道手机屏幕有三层,最外层是保护玻璃,中间是解控层,最下面是显示层。那么UIResponder对应的就是解控层,CALayer对应的就是显示层。UIView继承自UIResponder,拥有处理各种触摸事件的能力,又把layer对象当属性,有了显示内容的能力,然后在内部把两者结合从而成了UIView,拥有了通过触摸屏幕改变显示内容的能力。可以通俗的理解为,我们看到的都是layer,摸到的都是UIView。
layer负责显示那么我们关于显示操作的相关属性,其实本质都是在操作calyer,下面的一些截取UIKit源码可以说明这些。
- (CGRect)frame { return _layer.frame; } - (void)setFrame:(CGRect)newFrame { if (!CGRectEqualToRect(newFrame,_layer.frame)) { CGRect oldBounds = _layer.bounds; _layer.frame = newFrame; [self _boundsDidChangeFrom:oldBounds to:_layer.bounds]; [[NSNotificationCenter defaultCenter] postNotificationName:UIViewFrameDidChangeNotification object:self]; } } - (CGRect)bounds { return _layer.bounds; } - (void)setBounds:(CGRect)newBounds { if (!CGRectEqualToRect(newBounds,_layer.bounds)) { CGRect oldBounds = _layer.bounds; _layer.bounds = newBounds; [self _boundsDidChangeFrom:oldBounds to:newBounds]; [[NSNotificationCenter defaultCenter] postNotificationName:UIViewBoundsDidChangeNotification object:self]; } }
这部分是最常用的设置frame bounds的源码,可以到内部实现就是在改变layer的frame和bounds
- (void)addSubview:(UIView *)subview { NSAssert((!subview || [subview isKindOfClass:[UIView class]]), @"the subview must be a UIView"); if (subview && subview.superview != self) { UIWindow *oldWindow = subview.window; UIWindow *newWindow = self.window; [subview _willMoveFromWindow:oldWindow toWindow:newWindow]; [subview willMoveToSuperview:self]; if (subview.superview) { [subview.layer removeFromSuperlayer]; [subview.superview->_subviews removeObject:subview]; } [subview willChangeValueForKey:@"superview"]; [_subviews addObject:subview]; subview->_superview = self; [_layer addSublayer:subview.layer]; [subview didChangeValueForKey:@"superview"]; if (oldWindow.screen != newWindow.screen) { [subview _didMoveToScreen]; } [subview _didMoveFromWindow:oldWindow toWindow:newWindow]; [subview didMoveToSuperview]; [[NSNotificationCenter defaultCenter] postNotificationName:UIViewDidMoveToSuperviewNotification object:subview]; [self didAddSubview:subview]; } }
这是UIView必不可少的addSubView 方法,可以看到内部最主要的就是[_layer addSublayer:subview.layer];这一句话。
- (void)setHidden:(BOOL)h { if (h != _layer.hidden) { _layer.hidden = h; [[NSNotificationCenter defaultCenter] postNotificationName:UIViewHiddenDidChangeNotification object:self]; } } - (BOOL)isHidden { return _layer.hidden; }
综上代码可以看出,UIView凡是看到的都是layer,凡是操作与显示有关的本质都是在操作layer
UIView又是layer的代理,可以UIView里自定义绘制,流程如下
flow st=>start: [_layer drawInContext: (CGContextRef)context] op1=>operation: [_delegate drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx] op2=>operation: [view drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx] op3=>operation: [view drawRect] e=>end st->op1->op2->op3->e
UIView的laye对象绘制时会通过代理调用view的drawLayer方法,而在drawlayer方法里会调用drawRect方法,我们自定义UI时,继承UIView 实现drawRect方法即可。
-
关于frame和bounds
UIView有个必用的属性就是frame,是个CGRect类型。由一组坐标和一组size组成。size就是表示宽和高,坐标是基于父view,以父view左上为圆点。frame的概念非常好理解。但是bounds虽说也是CGRect类型的值,但是跟frame的概念完全不一样。bounds表示的是UIView的显示区域。改变x会让View的显示区域向左或向右偏移,改变y会让view的显示区域向上或者向下偏移。那改变size就是改变可显示区域的大小。我们熟悉的UIScrollView就是通过改变bounds实现的,scrollView使用时会设置它的contentSize,一般contentSize是大于scrollView本身的size的,scrollView基本原理就是添加一个拖动手势,在拖动时改变scrollView的bounds,来显示不同的内容。
-
关于autolayout
autolayout是现在主要的UI书写方式,frame最大的问题就是是个绝对布局,性能很好,就是非常不灵活,碰到宽高动态变化时就会非常的麻烦。autolayout是相对布局,通过各view之前添加约束,形成一个链条,当其中一个变化时,其它也会跟着自动变化,是比较优雅的UI书写方式。
autolayout并不是一种新UI实现方式,它最后还是以frame的方式落地。 它的原理就是在解多元一次方程,通过各约束条件构建方程,最后得到惟一的解,即它的最终frame。这就是父view可以通过上下左右的约束来确定子view的大小,子view也同样可以通过上下左右的约束来反推出父view的大小的理论依据。
-
-
应用部分
-
UIView 固有大小(intrinsicContentSize)
在写UI代码时, 我们会发现UILabel UIButton只需要添加位置确定的约束即可,无需添加大小相关的约束,这是因为UIView的intrinsicContentSize的属性,意为固有大小。作用就是当view没有明确大小时,就按照此属性的值来显示。UILabel,UIImageView,UIButton等这些组件及某些包含它们的系统组件都有 Intrinsic Content Size 属性,也就说他们都有自己计算size的能力。
我们也可利用此属性,来实现动态size变化的view。
实例说明:
需求:view包含有左右两个label,各占一半宽度,左右两个label高度随文字多少变化而变化,要求view高度取两个label最高高度。
实现:左右label各添加上下左右的边界约束,width equal,无需添加高度的约束。当某中一个label高度变化时,view的高度将会自动会依据较高的label来计算。
原理:当view的size是由子view推算出来时,view总是会取最大值,而又由于intrinsicContentSize的特殊性,所以当view的大小被确定时,其子view的size会根据父view的大小来确定。
-
优先级
AutoLayout添加的约束中也有优先级(Priority),优先级的数值1~1000,分为两种情况:
- 一种情况是我们经常添加的各种约束,默认值1000(最大值)优先执行,条件允许的话系统会自动满足我们的约束需求。
- 第二种就是固有约束(intinsic content size)严格说这种更像UILabel和UIButton的一种属性,但是在Autolayout中需要满足属性取值与约束优先级属性结合才能完成图形的绘制
当UILabel显示的内容过长或太短,控件就会被拉伸和压缩,当我们不想让控件被拉伸压缩时,就需要设置控件的固有约束(intinsic content size)来实现我们的需求。固有约束分为两种:
- 扛拉伸优先级(Content Hugging Priority):默认251,优先级越高越不易被拉伸
- 防压缩优先级(Content Compression Resistance Prority):默认750,优先级越高越不易被压缩
-
systemLayoutSizeFittingSize
有autolayout我们大多时候不需要关心宽高的问题,但总有些情况我们需要知道一些UI的总高度。比方说,tableViewheaderView,不支持autolayout,我们总需要给它设置好frame才能正确显示。
当我们用autolaout实现一个headerview时,就要知道它所需要的高度。此时就可用UIView 的一个方法
[view systemLayoutSizeFittingSize:CGSizeZero];需要一个CGSize参数,是预估大小,并不起决定性作用。使用此方法,要得到预期的真实大小,最底层的父view上下左右的约束一定要设置好。
注意:当一个View使用intrinsicContentSize来作为它的size,label或button之类的,那么使用此API时可能会得不到预期的效果,这时需要给它一个确定的宽或高,来获取预期的size。
-
view均分
需求:一个view有若干子view,子view大小相同,均匀的排布。
有两种实现方案:
-
各子view通过上下左右约束相互连接,宽度各相等的约束。
代码如下:
UIView *fatherView = [[UIView alloc]init]; UIView *tempView; for (int i = 0 ; i < 3; i ++) { UIView *view = [[UIView alloc]init]; [fatherView addSubview:view]; [view mas_makeConstraints:^(MASConstraintMaker *make) { make.top.bottom.mas_equalTo(0); if (tempView) { make.left.mas_equalTo(tempView.mas_right); make.width.mas_equalTo(tempView); } else{ make.left.mas_equalTo(0); } }]; tempView = view; } [tempView mas_makeConstraints:^(MASConstraintMaker *make) { make.right.mas_equalTo(0); }];
-
view宽度基于父view,通过centerX改变位置,centerX 可用公式来计算:CGFloat multi = (i*2.0+1.0)/totalCount;
代码如下:
UIView *fatherView = [[UIView alloc]init]; for (int i = 0 ; i < 3; i ++) { UIView *view = [[UIView alloc]init]; view.backgroundColor = [UIColor blueColor]; [fatherView addSubview:view]; [view mas_makeConstraints:^(MASConstraintMaker *make) { make.top.bottom.mas_equalTo(0); make.width.mas_equalTo(fatherView).multipliedBy(1.0/3.0); CGFloat multi = (i*2.0+1.0)/3; make.centerX.mas_equalTo(0).multipliedBy(multi); }]; }
第一种是通过相互的位置关系得到确定位置,可以随意设置各自的大小,间距等,但是比较适用于一行排满的情况。
第二种,是通过计算得到中心点以及宽度,比较适合一行分成多份,但是元素数量不够的情况。
在项目中,提供一个自定义UIFBUniformAutoWrapView,就是专门处理这种UI。
-
-
代码书写方案
在写UI代码时,推荐这种书写方式
- (void)darwView{ UIView *tempView; tempView = [self drawAViewWith:tempView]; if (1) { tempView = [self drawBViewWith:tempView]; } tempView = [self drawCViewWith:tempView]; tempView = [self drawDViewWith:tempView]; } - (UIView *)drawAViewWith:(UIView *)topView{ return nil; } - (UIView *)drawBViewWith:(UIView *)topView{ return nil; } - (UIView *)drawCViewWith:(UIView *)topView{ return nil; } - (UIView *)drawDViewWith:(UIView *)topView{ return nil; }
这么写有以下好处
- 代码的书写顺序就是真实的显示顺序,一一对应,更易阅读
- 只需要改变代码书写顺序,加逻辑判断,删除修改,都简单,更易维护
- 每块view只需关心传过来参数view的相对位置即可,传过来谁都一样,更易实现
-