Kealdish's Studio.

UIButton之titleLabel、imageView解析

字数统计: 1.5k阅读时长: 6 min
2015/12/28 Share

前言

UIButton是我们开发过程中使用频率很高的控件类。在使用UIButton实现需求时,通常会碰到需要改变UIButton中image和title位置和大小的需求。如果对UIButton中的titleLabel和imageView理解不到位的话,只能是不停地修改参数,不停地调试,陷入恶性循环,既低效又很难正确地去实现。为了解决这个问题,我研究出了两套方案,分别是用imageEdgeInsetstitleEdgeInsets组合的方案以及imageRectContentRettitleRectContentRect组合的方案。

titleEdgeInsets、imageEdgeInsets组合

这个方案是使用的比较多的解决方案,它主要依赖的是两个方法:

  • titleEdgeInsets
  • imageEdgeInsets

这个方案比较麻烦的地方在于很多人对这两个方法的理解不到位,导致使用的时候设置和显示的效果不一致。UIButton继承自UIControl,它因此也继承了两个属性contentVerticalAlignmentcontentHorizontalAlignment。这两个属性是用来排列内部元素的,默认值都是Center,先看下它们的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
UIControlContentVerticalAlignmentCenter = 0,
UIControlContentVerticalAlignmentTop = 1,
UIControlContentVerticalAlignmentBottom = 2,
UIControlContentVerticalAlignmentFill = 3,
};
typedef NS_ENUM(NSInteger, UIControlContentHorizontalAlignment) {
UIControlContentHorizontalAlignmentCenter = 0,
UIControlContentHorizontalAlignmentLeft = 1,
UIControlContentHorizontalAlignmentRight = 2,
UIControlContentHorizontalAlignmentFill = 3,
};

按照4*4的组合分为16种选择结果。为了在计算过程中更方便点,我通常选择UIControlContentVerticalAlignmentTopUIControlContentHorizontalAlignmentLeft的组合,这样符合我们以左上角作为坐标原点布局的思维。这里要纠正几个误区:第一,edgeinsets只是相对于当前位置的偏移量,并不是指距离UIButton边界的距离;第二,在同时存在图片还有文字的时候,只有UIButton的contentRect的宽度大于image和title的宽度才能正确显示,否则,文字由于无法进行拉伸收缩的原因只能显示…或者图片被压缩;第三、imageView的宽高都能被压缩,titleLabel的宽只能压缩不能拉伸,titleLabel的高只能拉伸不能压缩。

采用left-top方案初始情况下,image和title的位置如下图所示:

在初始状态下,imageEdgeInsetstitleEdgeInsets均为0,因此在计算偏移量的时候,image的top、left、right、bottom以及title的top、bottom、right均能以button的边界作为参考,而title的left是以image的left为参考的。搞清楚了之后,来做一下image在上,title在下的button:

1
2
3
4
5
6
7
8
9
CGFloat imageTop = (button.height-image.height-title.height) / 2;
CGFloat titleTop = imageTop+image.height;
CGFloat imageLeft = (button.width-image.width) / 2;
// 这里减了image.width,因为title的left是以image的left为参考
CGFloat titleLeft = (button.width-title.widht) / 2 - image.width;
button.imageEdgeInsets = UIEdgeInsetsMake(imageTop, imageLeft, 0, 0);
button.titleEdgeInsets = UIEdgeInsetsMake(titleTop, titleLeft, 0, 0);

这里还牵扯到一个问题,有人可能会觉得既然有left和top,那就不需要right和bottom了,实际不然。这实际上是涉及到约束的优先级。其定义如下:

  • top-left-bottom-right取负值 > 不能超出button边界 > imageView不能被压缩
  • top-left-bottom-right取负值 > 不能超出button边界 > titleLabel水平方向不能被压缩
  • titleLabel垂直方向不能被压缩 > 不能超出button边界

若有人运用发现image超出button边界,只需要同时调整contentEdgeInsets即可。

imageRectForContentRect、titleRectForContentRect组合

第一个方案理解之后确实可以解决问题,不过,我觉得有些麻烦,就想着能不能有更方便的方案。于是,我翻阅文档和资料后,找到了更简单的方案,也就是这第二个方案。

第二个方案涉及到两个方法:

  • (CGRect)titleRectForContentRect:(CGRect)contentRect
  • (CGRect)imageRectForContentRect:(CGRect)contentRect

根据文档的解释,这两个方法返回值是矩形区域,区域内则是用来绘制image和title,返回值的参照系都是UIButton。要想自定义image和title在UIButton的位置和大小,只需要继承UIButton,重写这两个方法即可。

相比第一个方案,第二个方案确实方便不少,但是又有一个问题。要是想改变一个UIButton都得继承UIButton重写方法显得太麻烦,实际上并没有达到简便的目的。于是,我想能不能用runtime运行时机制,给UIButton添加两个变量,直接设置就可以修改image和title在UIButton中的rect,这样会方便很多。

先贴上实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// UIButton+RectOfImageAndTitle.h
#import <UIKit/UIKit.h>
@interface UIButton (RectOfImageAndTitle)
@property (nonatomic,assign) CGRect btn_contentRect;
@property (nonatomic,assign) CGRect btn_imageRect;
@property (nonatomic,assign) CGRect btn_titleRect;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// UIButton+RectOfImageAndTitle.m
#import "UIButton+RectOfImageAndTitle.h"
#import <objc/runtime.h>
static const char *btn_contentKey = "btn_contentKey";
static const char *btn_imageKey = "btn_imageKey";
static const char *btn_titleKey = "btn_titleKey";
@implementation UIButton (RectOfImageAndTitle)
+ (void)load{
@autoreleasepool {
[self exchangeIMPWithOriginSelector:@selector(contentRectForBounds:) newSelector:@selector(zs_contentRectForBounds:)];
[self exchangeIMPWithOriginSelector:@selector(imageRectForContentRect:) newSelector:@selector(zs_imageRectForContentRect:)];
[self exchangeIMPWithOriginSelector:@selector(titleRectForContentRect:) newSelector:@selector(zs_titleRectForContentRect:)];
}
}
+ (void)exchangeIMPWithOriginSelector:(SEL)originSeletor newSelector:(SEL)newSelector{
if ([UIButton instancesRespondToSelector:originSeletor]) {
Method oriMethod = class_getInstanceMethod(self, originSeletor);
Method newMethod = class_getInstanceMethod(self, newSelector);
method_exchangeImplementations(oriMethod, newMethod);
}
}
#pragma mark - getter & setter
- (void)setBtn_contentRect:(CGRect)btn_contentRect{
objc_setAssociatedObject(self, &btn_contentKey, [NSValue valueWithCGRect:btn_contentRect], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGRect)btn_contentRect{
if (!objc_getAssociatedObject(self, &btn_contentKey)) {
objc_setAssociatedObject(self, &btn_contentKey, [NSValue valueWithCGRect:CGRectZero], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return [objc_getAssociatedObject(self, &btn_contentKey) CGRectValue];
}
- (void)setBtn_imageRect:(CGRect)btn_imageRect{
objc_setAssociatedObject(self, &btn_imageKey, [NSValue valueWithCGRect:btn_imageRect], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGRect)btn_imageRect{
if (!objc_getAssociatedObject(self, &btn_imageKey)) {
objc_setAssociatedObject(self, &btn_imageKey, [NSValue valueWithCGRect:CGRectZero], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return [objc_getAssociatedObject(self, &btn_imageKey) CGRectValue];
}
- (void)setBtn_titleRect:(CGRect)btn_titleRect{
objc_setAssociatedObject(self, btn_titleKey, [NSValue valueWithCGRect:btn_titleRect], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGRect)btn_titleRect{
return objc_getAssociatedObject(self, btn_titleKey) == nil ? CGRectZero : [objc_getAssociatedObject(self, btn_titleKey) CGRectValue];
}
#pragma mark - private method
- (CGRect)zs_contentRectForBounds:(CGRect)contentRect{
if (CGRectEqualToRect(self.btn_contentRect, CGRectZero)) {
return [self zs_contentRectForBounds:self.bounds];
}
else {
return self.btn_contentRect;
}
}
- (CGRect)zs_imageRectForContentRect:(CGRect)contentRect{
if (CGRectEqualToRect(self.btn_imageRect, CGRectZero)) {
return [self zs_imageRectForContentRect:self.bounds];
}
else {
return self.btn_imageRect;
}
}
- (CGRect)zs_titleRectForContentRect:(CGRect)contentRect{
if (CGRectEqualToRect(self.btn_titleRect, CGRectZero)) {
return [self zs_titleRectForContentRect:self.bounds];
}
else {
return self.btn_titleRect;
}
}
@end

简要说下实现的思路:由于在category中重写imageRectForContentRecttitleRectForContentRect会覆盖原来的方法实现,并且无法使用super关键字,因此重写方法行不通。转而我采用自己实现两个方法并分别与这两个方法的实现进行交换,这样就不会破坏原有方法的实现。此外,动态添加三个实例变量可以直接在实例对象中直接设置来调用imageRectForContentRecttitleRectForContentRect两个方法。

使用的时候导入头文件,再相应的设置一下实例变量就可以达到效果,有没有很方便。

TIPS

在开发过程中,我们经常会用到UIViewConentMode属性,有些人往往对属性值的意义不太了解,这里贴出一张图让你对此一目了然。

CATALOG
  1. 1. 前言
  2. 2. titleEdgeInsets、imageEdgeInsets组合
  3. 3. imageRectForContentRect、titleRectForContentRect组合
  4. 4. TIPS