IOS基础视图:大杂烩
知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、行间距
- 二、点击事件和手势冲突
一、行间距
1、设置行距
使用
[label setText:text lineSpacing:2.0f];
作为一个四处使用的工具方法,前面的nil
检查很有必要加。因为[[NSMutableAttributedString alloc] initWithString:text]
不接受nil
参数,会直接 crash
。生成的 paragraphStyle
除了配行距之外,还带上了 label
原有的一些常用属性。如果有其他需要,也可以加在这里。
- (void)setText:(NSString*)text lineSpacing:(CGFloat)lineSpacing {
if (lineSpacing < 0.01 || !text) {
self.text = text;
return;
}
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text];
[attributedString addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, [text length])];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineSpacing:lineSpacing];
[paragraphStyle setLineBreakMode:self.lineBreakMode];
[paragraphStyle setAlignment:self.textAlignment];
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [text length])];
self.attributedText = attributedString;
}
UITextView+Utils.m
使用
[textView setText:text lineSpacing:2.0f];
UITextView
的方法跟 UILabel
基本一样。
- (void)setText:(NSString*)text lineSpacing:(CGFloat)lineSpacing {
if (lineSpacing < 0.01 || !text) {
self.text = text;
return;
}
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text];
[attributedString addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, [text length])];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineSpacing:lineSpacing];
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [attributedText length])];
self.attributedText = attributedString;
}
2、计算行高
自定义行距之后,计算文本高度的方法也得相应改。很简单,只要利用 sizeToFit
、sizeThatFits
之类的方法就可以了。
UILabel+Utils.m
+ (CGFloat)text:(NSString*)text heightWithFontSize:(CGFloat)fontSize width:(CGFloat)width lineSpacing:(CGFloat)lineSpacing {
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, width, MAXFLOAT)];
label.font = [UIFont systemFontOfSize:fontSize];
label.numberOfLines = 0;
[label setText:text lineSpacing:lineSpacing];
[label sizeToFit];
return label.height;
}
UITextView+Utils.m
+ (CGFloat)text:(NSString*)text heightWithFontSize:(CGFloat)fontSize width:(CGFloat)width lineSpacing:(CGFloat)lineSpacing {
UITextView* textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, width, MAXFLOAT)];
textView.font = [UIFont systemFontOfSize:fontSize];
[textView setText:text lineSpacing:lineSpacing];
[textView sizeToFit];
return textView.height;
}
因为默认的 UITextView
有一点 inset
,所以计算文本高度的方法要跟 UILabel
分开。这几个方法就能应付大多数需求了。
3、代码上的行距 vs 设计图上的行距
这篇文章的重点其实是分享下面这一点:代码传参数进去的行距与设计图上量出来的行距是有区别的,代码上要少几个像素,而减少的量跟字体大小有关。
我感觉这一点有时容易被人忽视。例如一个 UILabel
字号为14,有些程序员可能就会把这个 Label
高度定为 14 像素了。而经验丰富的人就会知道不能这样,否则『h
』『g
』之类的字母都可能会被切掉一些。在xib
里,字号为 14 的 label
合适的高度应该是 17。
为了给像『g
』、『y
』英文字母的尾巴留出空间,系统会给 UILabel
上的文字上下加一点默认的空白,这就是 font size
与line height
的区别。而用代码设定paragraphStyle
的lineSpacing
,是叠加在原有空白之上的。
别小看这点空白。设计师设计出的行距往往也就是 4、5 个像素,而对 14 号字来说上下两行的空白就能占到 3 像素。如果不假思索地直接把设计图的标注传进去,结果就是行距放大到150%。视觉上出了偏差,我们也要负责任的。
由图所示,视觉上的行距其实由那 3 部分组成:上面一行的默认空白 + 行距 + 下面一行的默认空白。蓝色高度是我们写的 lineSpacing
,而黄色和绿色加起来正好是一倍font.lineHeight - font.pointSize
的值(黄色高度是上面一行的一半,为(font.lineHeight - font.pointSize
) / 2,绿色是下面一行的一半)。简单打下 log
就可以看到这个差值大概是多少。下面列出常见的字号:
font size | font.lineHeight(近似) | 差值 |
---|---|---|
10 | 12 | 2 |
11 | 13 | 2 |
12 | 14 | 2 |
13 | 15.5 | 2.5 |
14 | 17 | 3 |
15 | 18 | 3 |
16 | 19 | 3 |
17 | 20 | 3 |
18 | 21.5 | 3.5 |
19 | 23 | 4 |
20 | 24 | 4 |
为了计算效率高,我们就不在运行时现算这个差值了;直接把设计图上量出的行距减去上面这个表里几个像素的差值,作为参数传进去即可。例如:14 号字的 label,设计图上量出的行距是 5 个像素,那就减去 3 个像素,写[label setText:text lineSpacing:2.0f];
。不要忘了计算行高的时候也要用同样的参数。
二、点击事件和手势冲突
1、问题场景
使用TableView
写了一个登陆界面,帐号和密码两个Cell
中加入了TextField
。由于想在TableView
的空白处,点击时收起键盘,所以给self.view
添加一个UITapGestureRecognizer
来识别手势。然后发生了一个奇怪的现象,点击cell
无法选中,也就是tableView
的 didSelectRowAtIndexPath
没有反应了!底层到底是为什么会有点击事件的响应冲突呢?这一切的原因都是因为对于UITapGestureRecognizer
认识不够深刻。先写下几个结论,后面慢慢解释:(此处只讨论单击tap
事件)。
1、手势响应是大哥,点击事件响应链是小弟。单击手势优先于UIView
的事件响应。大部分冲突,都是因为优先级没有搞清楚。
2、单击事件优先传递给手势响应大哥,如果大哥识别成功,就会直接取消事件的响应链传递。
识别成功时候,手势响应大哥拥有垄断权力。(在斗地主里面叫做:吃肉淘汤。)如果大哥识别失败了,触摸事件会继续走传递链,传递给响应链小弟处理。
3、手势识别是需要时间的。手势识别有一个状态机的变化。在possible
状态的时候,单击事件也可能已经传递给响应链小弟了。
当你用一根手指触摸屏幕时会创建一个与之关联的UITouch
对象,一个手指第一次点击屏幕,就会生成一个UITouch
对象,到手指离开时销毁。一个UITouch
对象对应一根手指,所以可以直接,想象成是神功——一指禅。
一个UIEvent
事件定义为第一个手指开始触摸屏幕到最后一个手指离开屏幕。一个UIEvent
对象实际上对应多个UITouch
对象。所以,一个UIEvent
事件,可以简单的想象成是神功:如来神掌。(只是形象表示多个手指而已,不必要5个UITouch
事件组合。)
在iOS中不是任何对象都能处理事件,只有继承了UIResponder
的对象才能接收并处理事件,我们称为响应者对象。UIApplication
、UIViewController
、UIView
都继承自UIResponder
,因此他们都是响应者对象,都能够接收并处理事件,也就是说iOS中 所有的UIView
一旦成为响应者对象,都是可以响应单击的触摸事件的。
手势是Apple提供的更高级的事件处理技术,可以完成更多更复杂的触摸事件,比如旋转、滑动、长按等。基类是UIGestureRecognizer
。UIGestureRecognizer
同UIResponder
一样也有四个方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent: (nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
需要注意的是UIGestureRecognizer
是有状态的变化的。同一个手势是有具有多个状态的变化的,会形成一个有限状态机。左侧是非连续手势(比如单击)的状态机,右侧是连续手势(比如滑动)的状态机。所有的手势的开始状态都是UIGestureRecognizerStatePossible
。非连续的手势要么识别成功(UIGestureRecognizerStateRecognized
),要么识别失败(UIGestureRecognizerStateFailed
)。
连续的手势识别到第一个手势时,变成UIGestureRecognizerStateBegan
,然后变成UIGestureRecognizerStateChanged
,并且不断地在这个状态下循环,当用户最后一个手指离开view
时,变成UIGestureRecognizerStateEnded
,当然如果手势不再符合它的模式的时候,状态也可能变成UIGestureRecognizerStateCancelled
。
2、手势识别与事件响应混用
触摸事件可以通过响应链来传递与处理,也可以被绑定在view
上的手势识别和处理。那么这两个一起用会出现什么问题?
图中baseView
有两个subView
,分别是testView
和testBtn
。我们在baseView
和testView
都重载touchsBegan:withEvent
、ouchsEnded:withEvent
、
touchsMoved:withEvent
、touchsCancelled:withEvent
方法,并且在baseView
上添加单击手势,action
名为tapAction
,给testBtn
绑定action
名为testBtnClicked
。
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
[self.view addGestureRecognizer:tap];
...
[_testBtn addTarget:self action:@selector(testBtnClicked) forControlEvents:UIControlEventTouchUpInside];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Began");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Moved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Ended");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Cancelled");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Began");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Moved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Ended");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Cancelled");
}
情景A :单击baseView
,输出结果为:
=========> base view touchs Began
=========> single Tapped
=========> base view touchs Cancelled
情景B :单击testView
,输出结果为:
=========> test view touchs Began
=========> single Tapped
=========> test view touchs Cancelled
情景C :单击testBtn
, 输出结果为:
=========> click testbtn
情景D :按住testView
,过5秒后或更久释放,输出结果为:
=========> test view touchs Began
=========> test view touchs Ended
情景A和B
情景A和B,都是在单击之后,既响应了手势的tap 事件,也让响应链方法执行了。为什么两个响应都执行了呢?首先,我们的单击事件,是由手势识别这个大哥来优先获取。只不过,手势识别是需要一点时间的。在手势还是Possible
状态的时候,事件传递给了响应链的第一个响应对象(baseView
或者 testView
)。这样自然就去调用了,响应链UIResponder
的touchsBegan:withEvent
方法,之后手势识别成功了,就会去cancel
之前传递到的所有响应对象,于是就会调用它们的touchsCancelled:withEvent:
方法。
情境C
情景A和B都可以解释明白了。但是,请注意,按这样的解释为什么情景C没有触发响应链的方法呢?在情境C,里面testBtn
的默认action
,获取了事件响应,不会把事件传递给父视图baseView
,自然就不会触发,baseView
的tap
事件了。
情境D
在情景D中,由于长按住testView
不释放,tap
手势就会识别失败,因为长按就已经不是单击事件了。手势识别失败之后,就可以继续正常传递给testView
处理。所以,只有响应链的方法触发了。
实际开发遇到的问题解决
基本的开发目标,不让父视图的手势识别干扰子视图UIView
的点击事件响应或者说响应链的正常传递。一般都会是重写UIGestureRecognizerDelegate
中的- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
方法。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// 若为UITableViewCellContentView(即点击了tableViewCell),
if ([NSStringFromClass([touch.view class]) isEqualToString:@"UITableViewCellContentView"]) {
// cell 不需要响应 父视图的手势,保证didselect 可以正常
return NO;
}
// 默认都需要响应
return YES;
}