UIView(控件)
UIView的创建
-
alloc init
方法创建
- 通过
UIStoryboard加载
控件详见UIStoryboard - 通过
xib加载
控件详见XIB
UIView的常用属性
// superview:获得自己的父控件对象,每个子控件的superview只有一个,该属性只读
// 一般用于判断是否已经加入到视图中,避免重复添加
if (view.superview) return;
// subviews:获得自己的所有子控件对象,只读
NSLog(@"%@",view.subviews);
// 是否可与用户交互
view.userInteractionEnabled = YES;
// 裁剪超出范围的控件
view.clipsToBounds = YES;
// 设置透明度
view.alpha = 1;
// 设置背景颜色,默认是clearColor
view.backgroundColor = [UIColor colorWithRed:1 green:1 blue:0 alpha:0.3];//颜色由三原色组成
- [UIView 属性解析大全][http://www.jianshu.com/p/4baf286f4bc1]
UIView的常用方法
// 添加一个子控件view
// 底层实现:
// 1.判断传入view之前有没有父控件
// 2.如果有,先移除之前父控件
// 3.再重新添加view
[self.view addSubview:view];
// 从父控件中移除
// 多用于动画控件完成动画后移除
[self.view removeFromSuperview];
UIView获取子控件或其标识
- 开发中,经常需要使用到标识来获取控件或其标识,用于控件间的联动,这时的可选方案如下:
-
方案一:通过tag值(不推荐使用)
- tag:控件的ID(标识),父控件可以通过tag来找到对应的子控件;
- tag默认值是0
- tag值是随意设置,很容易混乱,能不用就不用,开发时间越长,tag值管理会越来越困难
// 设置tag值 view.tag = 10; // 根据tag获取view,没找到返回nil UIView *view = [self.view viewWithTag:10]; // viewWithTag:内部的大致实现思路 - (UIView *)viewWithTag:(NSInteger)tag { if (self.tag == tag) return self; for (UIView *subview in self.subviews) { // 返回tag标识找出对应的控件(一般都是子控件) return [subview viewWithTag:tag]; } }
-
方案二:通过subviews数组获取(不推荐)
- 不足:一般都是通过控制器的view的subviews获取子控件或其标识,但该view不仅有我们自己创建的控件,也有系统创建的控件,不好把握,容易出错;另外子元素种类一般不相同,不方便管理
// 获取某个子控件 UIView *view = self.view.subviews[0]; // 获取控件标识 NSInteger index = [self.views indexOfObject:view]
- 不足:一般都是通过控制器的view的subviews获取子控件或其标识,但该view不仅有我们自己创建的控件,也有系统创建的控件,不好把握,容易出错;另外子元素种类一般不相同,不方便管理
-
方案三:新建数组,获取角标获取控件(推荐方案)
// 新建控件数组并作为成员属性 NSMutableArray *views = [NSMutableArray array]; self.views = views; // 将控件添加到控件数组中 [self.views addObject:view]; [self.views addObject:view1]; [self.views addObject:view2]; // 获取控件标识 NSInteger index = [self.views indexOfObject:view] // 获取控件 UIView *view = self.views[index];
-
UIViewContentMode属性(只对UIImageView控件中图片有效)
- 带有
scale
单词的:图片有可能会拉伸(常用)
含scale 的属性 |
拉伸方式(不旋转图片 ) |
保持原宽高比? | 备注 |
---|---|---|---|
UIViewContentModeScale ToFill |
不按比例拉伸 拉伸图片至填充整个控件 |
否 | 图片跟控件同宽同高 ;默认属性
|
UIViewContentModeScaleAspect Fill |
按比例拉伸 图片,直至图片拉伸至填充整个控件 |
是 | 图片一边与控件相等,另一边大 于控件 |
UIViewContentModeScaleAspect Fit |
按比例拉伸 图片,直至图片任一边与控件对应边一样大 |
是 | 图片一边与控件相等,另一边小 于控件 |
- 没有
scale
单词的:图片绝对不会被拉伸
不含scale 的属性 |
是否拉伸 | 在控件中的显示位置 |
---|---|---|
UIViewContentModeCenter | 保持图片的原尺寸 | 居中 |
UIViewContentModeTop | 同上 | 上面 |
UIViewContentModeBottom | 同上 | 下面 |
UIViewContentModeLeft | 同上 | 左面 |
UIViewContentModeRight | 同上 | 右面 |
UIViewContentModeTopLeft | 同上 | 左上 |
UIViewContentModeTopRight | 同上 | 右上 |
UIViewContentModeBottomLeft | 同上 | 左下 |
UIViewContentModeBottomRight | 同上 | 右下 |
UIView的封装(自定义控件)
- 自定义控件基础:父子控件
1. 每个控件都是个容器,能容纳其他控件
1. 内部小控件是大控件的子控件
2. 大控件是内部小控件的父控件
-
自定义控件的意义
1. 如果一个view内部的子控件比较多,一般会考虑自定义一个view,把它内部子控件的创建屏蔽起来,不暴露给外界2. 外界可以传入对应的模型数据给view,view拿到模型数据后给内部的子控件设置对应的数据
-
步骤:
- 新建一个继承
UIView
的类
- 创建并布局子控件(纯代码或者xib方式)
纯代码方式
1. 提供类方法及对象方法
1. 可以重写init或initWithFrame方法
2. 重写init创建时,底层也会调用initWithFrame方法,先调用父类的,再调用子类自身;
2. 在initWithFrame:方法中添加并初始化子控件
3. 在layoutSubviews方法中布局子控件,布局前必须调用[super layoutSubviews];xib方式
1. 新建一个xib文件(xib的文件名最好跟控件类名一样)
1. 添加子控件、设置子控件属性
2. 修改最大的父控件的class为控件类名
3. 将子控件进行连线
2. 提供一个类方法及对象方法用于加载xib
1. 初始化时底层不调用initWithFrame:方法,而是调用`initWithCoder:方法。加载时:
2. initWithCoder是第一个执行的方法
3. awakeAfterUsingCoder:是第二个执行的方法
3. awakeFromNib是最后执行的方法,可在这里初始化控件
- 提供
模型属性
,重写模型属性的set
方法- 在set方法中给子控件设置数据
- 新建一个继承
渐变式动画
- 头尾式
// 开始动画 [UIView beginAnimations:nil context:nil]; // 设置动画属性 // 设置动画时间 [UIView setAnimationDuration:2.0]; /* 需要执行动画的代码 */ // 提交动画 [UIView commitAnimations];
- block式(实质是苹果对头尾式动画的封装)
// 在1秒内执行动画 [UIView animateWithDuration:1.0 animations:^{ // 显示指示版 self.hudLbl.alpha = 1;//不透明 } completion:^(BOOL finished) { // 上一个动画完毕后延迟3秒后,在1秒内执行下列动画 [UIView animateWithDuration:1.0 delay:3.0 options:kNilOptions animations:^{ // 隐藏指示版 self.hudLbl.alpha = 0;//完全透明 } completion:nil]; }];
UIView界面跳转
如果2个控件如果有重叠部分,那么处于上面的那个控件会盖住下面的。上面的控件不是透明或颜色为clearColor,会挡住下面控件的显示
开发中需要在某些时刻将指定的控件显示出来,即放在最上面;这时需要界面跳转。
- 界面跳转主要的方法有3种:
- 用 UINavigationController 把 View B push 进来。
[self.navigationController pushViewController:nextView animated:YES];
- 用Modal的 presentModalViewController 方法把 View B 盖在上面。
[self presentModalViewController:nextView animated:YES];
- 调整窗口subView中元素的顺序,也可应用于当前显示的父控件中,让其某个子控件显示出来;
// AppDelegate.m 类 // 添加到窗口上 [self.window addSubview:viewB]; [self.window addSubview:viewA]; // 调整控件位置顺序,让其显示出来 [self.window bringSubviewToFront:viewB];
- 用 UINavigationController 把 View B push 进来。
- 界面跳转的本质重新调整窗口subViews中元素的顺序,让最后一个元素显示
UIView圆角裁剪和加边框方式
方案一 :最简单的
// 最简单的方式就是设置每个 View 自带的 layer 的属性即可
view.layer.cornerRadius = 8.0f;
// 如果该 View 有子 View,会是这种状况
// 如果需要切掉除了保留部分以外的子 View,那么需要加上
view.clipsToBounds = YES;
// 如果需要边框,也简单,加上 layer 的边框设置就可以。
view.layer.borderWidth=10.0f;
view.layer.borderColor= [UIColor yellowColor].CGColor;
// 但是如果细看,会发现边框有严重的黑边,特别是当 View 背景色比较深的时候
方案一总结:
1.1 好处:简单,哪里需要圆角就在哪里设置就可以了。
1.2 缺点:这种方式处理的圆角很模糊,特别是在视图比较小的时候,质量不高,当背景色比较深的时候有边框有黑边现象,而且如果使用了 clipToBounds ,则会触发离屏渲染(这个一个很大的坑,有兴趣的话可以详细了解下),造成很严重的卡顿问题,特别是在 UITableViewCell 的子 View 中这样使用,掉帧会很严重。
方案二 :设置 mask layer
// 使用贝赛尔曲线,并根据其路径,得到一个「遮罩 layer」,将其设置为 View 自带 layer 的 mask,盖掉其他部分,剩余中间的部分。也可以达到圆角效果。
- (void)setRoundedCorners {
UIBezierPath* maskPath = [UIBezierPathbezierPathWithRoundedRect:self.bounds byRoundingCorners:corners cornerRadii:size];
CAShapeLayer* maskLayer = [CAShapeLayerlayer];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
}
// 将这个 masklayer 加到自带 layer 上而不是设置为其 mask,代码如下:
/*
UIBezierPath* maskPath = [UIBezierPath bezierPathWithRoundedRect:v.boundsbyRoundingCorners:UIRectCornerAllCornerscornerRadii:CGSizeMake(halfW, halfW)];
CAShapeLayer* maskLayer = [CAShapeLayer layer];
maskLayer.frame = view.bounds;
maskLayer.fillColor = [UIColor whiteColor].CGColor;
maskLayer.path = maskPath.CGPath;
maskLayer.backgroundColor= [UIColor greenColor].CGColor;
[view.layer addSublayer:maskLayer];
*/
// 效果图:
// 一个只有四个角的 layer 盖到原来的 layer 上,达到圆角效果。
// 因为是盖住四角达到的效果,所以不用设置 maskToBounds 也可以去掉子 View 超出中心圆的部分,但是同样会触发离屏渲染。用的时候应当小心。
```

```objc
// 如果要加上边框,可以在 self.layer 上加一个圆环 layer 达到边框效果,代码如下:
- (void)setRoundedCorners:(UIRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(UIColor*)borderColor cornerSize:(CGSize)size {
UIBezierPath* maskPath = [UIBezierPathbezierPathWithRoundedRect:self.bounds byRoundingCorners:corners cornerRadii:size];
CAShapeLayer* maskLayer = [CAShapeLayerlayer];
maskLayer.fillColor = [UIColorblueColor].CGColor;
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
if(borderWidth >0) {
CAShapeLayer*borderLayer = [CAShapeLayerlayer];
// 用贝赛尔曲线画线,path 其实是在线的中间,这样会被 layer.mask(遮罩层)遮住一半,故在 halfWidth 处新建 path,刚好产生一个内描边
CGFloathalfWidth = borderWidth /2.0f;
CGRectf =CGRectMake(halfWidth, halfWidth,CGRectGetWidth(self.bounds) - borderWidth,CGRectGetHeight(self.bounds) - borderWidth);
borderLayer.path = [UIBezierPathbezierPathWithRoundedRect:f byRoundingCorners:corners cornerRadii:size].CGPath;
borderLayer.fillColor = [UIColorclearColor].CGColor;
borderLayer.strokeColor = borderColor.CGColor;
borderLayer.lineWidth = borderWidth;
borderLayer.frame = CGRectMake(0,0,CGRectGetWidth(f),CGRectGetHeight(f));
[self.layer addSublayer:borderLayer];
}
// 我们会看到这里 borderLayer 的坐标很奇怪,这里解释一下,如果我们这样设置坐标,用以下方式添加 borderLayer,会是什么效果呢?
UIBezierPath* borderPath= [UIBezierPathbezierPathWithRect:v.bounds];
CAShapeLayer* borderLayer= [CAShapeLayer layer];
borderLayer.path =borderPath.CGPath;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.strokeColor = [[UIColorblackColor]colorWithAlphaComponent:0.5].CGColor;
borderLayer.lineWidth =10;
borderLayer.frame = v.bounds;
[v.layer addSublayer:borderLayer];
// 效果图:
// 效果分析:
// 边框刚好骑在了边界上,如果这时候我们在使用 maskLayer 做圆角,那骑在边界上的边框有外面一半将被吃掉,只剩下一半,所以要把 borderLayer 往里挪一半的边框的距离,避免让 maskLayer 吃掉外面那部分边框。
放大后的效果图:
方案二总结:
1.1 好处:简单,并且可以使用贝赛尔曲线达到任意形状的「剪切」,也可以根据情况选择要「切」的角,比如如果只需要切右上和右下两个角,那么只需要改一下贝赛尔曲线
UIBezierPath* maskPath = [UIBezierPathbezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopRight|UIRectCornerBottomRightcornerRadii:CGSizeMake(halfW, halfW)];
1.2 缺点:上一种方式的缺点,这个方式也都有
方案三 :生成圆角背景图片方式
CGFloat w = 200;
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0,0, w, w)];
v.backgroundColor= [UIColor redColor];
[self.view addSubview:v];
v.center = self.view.center;
UIImage *img = [UIImage imageWithColor:v.backgroundColorandSize:v.bounds.size];
img = [img roundedWithBorderWidth:10borderColor:[UIColorgreenColor]];
UIImageView *imageView = [[UIImageView alloc] initWithImage:img];
[v insertSubview:imageViewatIndex:0];
v.backgroundColor= [UIColor clearColor];
// 根据 View 的大小和背景色,生成一张图片,生成图片的方法如下:
@implementationUIImage(ZQExtension)
+ (UIImage*)imageWithColor:(UIColor*)color andSize:(CGSize)size {
CGRectrect =CGRectMake(0.0f,0.0f, size.width, size.height);
UIGraphicsBeginImageContextWithOptions(rect.size,NO, [UIScreenmainScreen].scale);
CGContextRefcontext =UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [colorCGColor]);
CGContextFillRect(context, rect);
UIImage*image =UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); returnimage;
}
@end
// 再将这张图片圆角处理,并加上边框,最后创建一个 UIImageView,设置该 imageView 的 image 为圆角处理后的图片,并插入 View 的最底层,造成一种圆角处理的假象。
// 圆角处理的代码如下:
@implementationUIImage(ZQExtension)
- (UIImage*)roundedWithBorderWidth:(CGFloat)borderWidth borderColor:(UIColor*)borderColor {
CGFloatinset =1;
CGFloatwidth =self.size.width;
CGFloatheight =self.size.height;
CGFloatcornerRadius;
UIBezierPath*maskShape;
if(width > height) {
cornerRadius = height / 2.0- inset;
maskShape = [UIBezierPathbezierPathWithRoundedRect:CGRectMake((width-height)/2.0+ inset,0+ inset, height-2*inset, height-2*inset) cornerRadius:cornerRadius];
}else{
cornerRadius = width / 2.0- inset;
maskShape = [UIBezierPathbezierPathWithRoundedRect:CGRectMake(0+inset, (height-width)/2.0+inset, width-2*inset, width-2*inset) cornerRadius:cornerRadius];
}
UIGraphicsBeginImageContextWithOptions(self.size,NO, [UIScreenmainScreen].scale);
CGContextRefctx =UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
CGContextAddPath(ctx, maskShape.CGPath);
CGContextClip(ctx); CGContextTranslateCTM(ctx,0, height);
CGContextScaleCTM(ctx,1.0,-1.0);
CGContextDrawImage(ctx,CGRectMake(0,0, width, height),self.CGImage);
CGContextRestoreGState(ctx);
if(borderWidth >0) {
[borderColor setStroke];
CGFloathalfWidth = borderWidth /2.0;
UIBezierPath*border = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(halfWidth, halfWidth,self.size.width - borderWidth ,self.size.width - borderWidth)];
CGContextSetShouldAntialias(ctx,YES);
CGContextSetAllowsAntialiasing(ctx,YES);
CGContextSetLineWidth(ctx, borderWidth);
CGContextAddPath(ctx, border.CGPath);
CGContextStrokePath(ctx);
}
UIImage*resultingImage =UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
returnresultingImage;
} @end
// 效果如下:
// 这里使用贝赛尔曲线加边框的坐标和第二种方式的类似。
方案三 :生成圆角背景图片方式总结
3.1 好处:是不会触发离屏渲染,生成图片和圆角都由 CPU 处理,且边框清晰,没有黑边,
3.2 缺点:不能处理子 View,在四角处的子 View 不会被「切」掉,毕竟是一个圆角背景造成的圆角假象,对子 View 没有什么影响力。
方案四 :使用遮罩层(推荐)
// 这个遮罩层不是上面提遮罩 layer,是一张你想要保留的形状的一张图片,比如想要圆角图片,可以让设计师做一张这样图片:
// 中间透明,周围是想要盖住的形状,最中看到的是中间留下来的形状,这种做法没有什么性能损耗,就是需要麻烦设计师做一张图。
// 具体的做法见,也可以跑一下这个 demo 看看怎么对 UITableView 优化的。 https://github.com/johnil/VVeboTableViewDemo/blob/master/VVeboTableViewDemo/view/VVeboTableViewCell.m#L46
// 设计师需要做的图:
五、总结:UIView圆角裁剪和加边框方式汇总
5.1 在[离屏渲染优化][http://www.jianshu.com/p/ca51c9d3575b] (建议好好看看篇文章)中,seedante 对各种圆角方案也有总结对比,最终得到这样一个结果:
5.2 任何时候优先考虑避免触发离屏渲染,无法避免时优化方案有两种:
5.3 Rasterization:适用于静态内容的视图,也就是内部结构和内容不发生变化的视图,对上面的所有效果而言,在实现成本以及性能上最均衡的。即使是动态变化的视图,开启 Rasterization 后能够有效降低 GPU 的负荷,不过在动态视图里是否启用还是看 Instruments 的数据。
5.4 规避离屏渲染,用其他手法来模拟效果,混合图层是个性能最好、耗能最少的通用优化方案,尤其对于 rounded corer 和 mask。
5.5 总的来说,圆角方案需要根据情况具体选择用哪种方式。
UIView的layout机制解释
// ios layout机制相关方法
- (CGSize)sizeThatFits:(CGSize)size
- (void)sizeToFit
——————-
- (void)layoutSubviews
- (void)layoutIfNeeded
- (void)setNeedsLayout
——————–
- (void)setNeedsDisplay
- (void)drawRect
/*
layoutSubviews在以下情况下会被调用:
1、init初始化不会触发layoutSubviews
但是是用initWithFrame 进行初始化时,当rect的值不为CGRectZero时,也会触发
2、addSubview会触发layoutSubviews
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
4、滚动一个UIScrollView会触发layoutSubviews
5、旋转Screen会触发父UIView上的layoutSubviews事件
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件
在苹果的官方文档中强调:
You should override this method only if the autoresizing behaviors of the subviews do not offer the behavior you want.
layoutSubviews, 当我们在某个类的内部调整子视图位置时,需要调用。
反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。
刷新子对象布局
-layoutSubviews方法:这个方法,默认没有做任何事情,需要子类进行重写
-setNeedsLayout方法: 标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用
-layoutIfNeeded方法:如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)
如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局
在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]
重绘
-drawRect:(CGRect)rect方法:重写此方法,执行重绘任务
-setNeedsDisplay方法:标记为需要重绘,异步调用drawRect
-setNeedsDisplayInRect:(CGRect)invalidRect方法:标记为需要局部重绘
sizeToFit会自动调用sizeThatFits方法;
sizeToFit不应该在子类中被重写,应该重写sizeThatFits
sizeThatFits传入的参数是receiver当前的size,返回一个适合的size
sizeToFit可以被手动直接调用
sizeToFit和sizeThatFits方法都没有递归,对subviews也不负责,只负责自己
———————————-
layoutSubviews对subviews重新布局
layoutSubviews方法调用先于drawRect
setNeedsLayout在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews
layoutIfNeeded方法如其名,UIKit会判断该receiver是否需要layout.根据Apple官方文档,layoutIfNeeded方法应该是这样的
layoutIfNeeded遍历的不是superview链,应该是subviews链
drawRect是对receiver的重绘,能获得context
setNeedDisplay在receiver标上一个需要被重新绘图的标记,在下一个draw周期自动重绘,iphone device的刷新频率是60hz,也就是1/60秒后重绘
*/