浅析UIButton的imageEdgeInsets与titleEdgeInsets

导语

系统的 UIButton 默认状态下的样式是图标在左标题在右,但有时候可能需要不同的排版。当然可以通过继承添加子视图来实现需求,但本文打算通过理解 UIButton 自带的 imageEdgeInsetstitleEdgeInsets 属性实现该功能。

主要内容包含以下两点:

  • 浅析 imageEdgeInsetstitleEdgeInsets 的属性的原理 [个人观点]
  • 简单实现图标在右标题在左,图标在上标题在下。

环境

macOS Sierra 10.12.4
Xcode 8.3.2
iPhone 6S (10.1.1)

流程

先从苹果官方对该方法的注释入手

The inset or outset margins for the rectangle around the button’s title text.

使用此属性可调整按钮标题的有效绘图矩形的大小并重新定位。(来自 google 翻译)

Use this property to resize and reposition the effective drawing rectangle for the button title. You can specify a different value for each of the four insets (top, left, bottom, right). A positive value shrinks, or insets, that edge—moving it closer to the center of the button. A negative value expands, or outsets, that edge. Use the UIEdgeInsetsMake function to construct a value for this property. The default value is UIEdgeInsetsZero.

关于 UIEdgeInsetsMaketop, left, bottom, right正数表明更靠近按钮的中心,负数表示更靠近按钮的边缘,默认为 UIEdgeInsetsZero

问题 1

  • margins 是边距的含义,那原始的位置在哪?

测试代码

#import <UIKit/UIKit.h>

@interface JAButton : UIButton

@end

#import "JAButton.h"

@implementation JAButton
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        self.layer.borderColor = [UIColor blueColor].CGColor;
        self.layer.borderWidth = 1;
        [self setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
        
        // [1]
        [self setTitle:@"测试" forState:UIControlStateNormal];
        
        // [2]
        [self setImage:[UIImage imageNamed:@"arrow"] forState:UIControlStateNormal];
    }
    return self;
}
- (void)layoutSubviews {
    [super layoutSubviews];
    
    // [3]
    self.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 10);
    
    // [4]
    self.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 10);
}
@end

...

JAButton *b = [[JAButton alloc] initWithFrame:CGRectMake(10, 100, 100, 40)];
[self.view addSubview:b];

测试 1

只有标题 [注释 2 3 4]

标题的位置如下


标题居中

测试 2

只有图标 [注释 1 3 4]

图标位置信息如下


图标居中

测试 3

标题 + 图标 [注释 3 4]

图标


标题


为了维持居中就要将标题和图标看做一个整体即宽度和为 37 + 10 = 47 ,用 UIButton 的宽度来减去总宽度再乘以 0.5 即可实现整体居中,因此图标的 X 就是 (100 - 47) * 0.5 = 26.5

测试 4

只调整 titleEdgeInsets [注释 2 4]

标题的位置信息如下

测试 5

只调整 imageEdgeInsets [注释 1 3]

图标的位置信息如下

问题 2

比较测试 4 与测试 1 会发现

  • titleEdgeInsetsright 设置了 10 , X 却向左偏移了 5 ?

同样对比 测试 5 与测试 2 会发现

  • imageEdgeInsetsright 设置了 10 , X 却向左偏移了 5

为什么会有这种减半的现象呢?

寻找 & 分析

在搜索后,这篇 博客对我有所启发

回答 1

它们只是image和button相较于原来位置的偏移量,那什么是原来的位置呢?就是这个

没有设置edgeInset时候的位置了。
如要要image在右边,label在左边,那image的左边相对于button的左边右移了labelWidth的距离,image的右边相对于label的左边右移了labelWidth的距离
所以,self.oneButton.imageEdgeInsets = UIEdgeInsetsMake(0, labelWidth, 0, -labelWidth); 为什么是负值呢?因为这是contentInset,是偏移量,不是距离
同样的,label 的右边相对于 button 的右边左移了 imageWith 的距离,label 的左边相对于 image 的右边左移了 imageWith 的距离
所以 self.oneButton.titleEdgeInsets = UIEdgeInsetsMake(0, -imageWith, 0, imageWith); 这样就完成image在右边,label在左边的效果了。

但是对下面的前置知识点,感觉有些疑惑

前置知识点:titleEdgeInsets是title相对于其上下左右的inset,跟tableView的contentInset是类似的,如果只有title,那它上下左右都是相对于button的,image也是一样;
如果同时有image和label,那这时候image的上左下是相对于button,右边是相对于label的;title的上右下是相对于button,左边是相对于image的。

我认为虽然两者都在 UIButton 中,但 Apple 既然将 imageEdgeInsetstitleEdgeInsets 拆成两个属性,两者的位置应该不互相依赖才对,即使依赖,也应该依赖 UIButton 这个父视图比较合适。

测试 6

在标题和图标同时存在的情况下,调整 titleEdgeInsets [注释 4]

标题

图标


测试 7

在标题和图标同时存在的情况下,调整 imageEdgeInsets [注释 3]

标题


图标

测试 8

在标题和图标同时存在的情况下,调整 imageEdgeInsetstitleEdgeInsets [不注释]

标题


图标


小结

将测试 3 和 测试 6 或 测试 3 和测试 7 对比会发现即使在标题和图标同时存在的情况下,单独调整 imageEdgeInsetstitleEdgeInsets 都只会对对应的视图的位置产生影响,而且影响同样是 减半 的。而通过将测试 3 和 测试 8 比较, titleEdgeInsetsimageEdgeInsets 同时作用的情况下也是一样的。

佐证

上面的参考博客中提到 Aligning text and image on UIButton with imageEdgeInsets and titleEdgeInsetsStackOverflow 上关于这个问题的一个讨论。里面有这样一段话,对我有所启发

I believe that this documentation was written imagining that the button has no title, just an image. It makes a lot more sense thought of this way, and behaves how UIEdgeInsets usually do. Basically, the frame of the image (or the title, with titleEdgeInsets) is moved inwards for positive insets and outwards for negative insets。

官方的注释也许正如上面这段话所表达的,只是在告诉我们 imageEdgeInsets/titleEdgeInsets 其实只是描述了父视图(UIButton)与它们各自视图的间距。

回答 2

在理解了 imageEdgeInsets/titleEdgeInsets 的独立性后,我尝试用自己的话来说明为什么会存在"减半"的效果。

比如下面的代码

self.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 10);

在水平方向上 titleLabel 左侧偏移量为 0titleLabel 右侧偏移量为 10 ,但是偏移量 X 的变化量。

X 左移 10 那么实际上的偏移属性值应该为 (0,-10,0,10) :

左偏移量(相对于 button 左侧),title 从原始向目标是往边缘方向,所以是负值(参考本文最初的文档理解),偏移量为 10,右偏移量(相对 button 右侧),从原始到目标是往中间方向,所以是正值。

因此上面的代码表明 titleLabel 相对 UIButton 左侧不改变,右侧改变 10 ,应该是做不到的,左侧间距增加1,右侧间距必然会减少1。会被等价转换为

self.titleEdgeInsets = UIEdgeInsetsMake(0, -5, 0, 5);

个人推测

如何转换?

参考 iOS 的坐标系,水平向右为 X 轴正方向 ,垂直向下为 Y 正方向,要根据 titleEdgeInsets / imageEdgeInsets 去计算 titleimage 的坐标,可以对 UIEdgeInsets 结构体的四个成员( top , left , bottom , right ) 进行处理 (在负方向留正偏移量即是往正方向偏移)。

公式如下

  • 水平方向上 X 的偏移量: (left + (-1) * right) / 2
  • 垂直方向上 Y 的偏移量: (top + (-1) * bottom) / 2

直白点的话: 负间距(left,top)更靠近,正间距(left,top)更远离,原始状态就是 UIEdgeInsets 全为 0 即你不去操作 titleEdgeInsets / imageEdgeInsets 时。

个人推测

实践

实践是检验真理的唯一标准;不管黑猫白猫,能抓老鼠的就是好🐱...

图标在右标题在左

根据上面的猜想,要实现图标与标题的位置交换,很简单: imageView 左侧相对于 UIButton 向中央移动了 titleLabel 的宽度,记为 titleLabel.w,右侧相对于 UIButton 向边缘同样移动了 titleLabel.w 因此

- (void)layoutSubviews {
    [super layoutSubviews];
    
    // [3]
//    self.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 10);
//    
//    // [4]
//    self.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 10);
    
    self.imageEdgeInsets = UIEdgeInsetsMake(0, self.titleLabel.frame.size.width, 0, -self.titleLabel.frame.size.width);
    self.titleEdgeInsets = UIEdgeInsetsMake(0, -self.imageView.frame.size.width, 0, self.imageView.frame.size.width);
}

标题


图标


但是如果用直接去设置 UIButton

UIButton *b = [[UIButton alloc] initWithFrame:CGRectMake(10, 100, 100, 40)];
b.layer.borderColor = [UIColor blueColor].CGColor;
b.layer.borderWidth = 1;
[b setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
[b setTitle:@"测试" forState:UIControlStateNormal];
[b setImage:[UIImage imageNamed:@"arrow"] forState:UIControlStateNormal];
    [self.view addSubview:b];
    b.imageEdgeInsets = UIEdgeInsetsMake(0, b.titleLabel.frame.size.width, 0, -b.titleLabel.frame.size.width);
    b.titleEdgeInsets = UIEdgeInsetsMake(0, -b.imageView.frame.size.width, 0, b.imageView.frame.size.width);

你会发现效果并不如意,原因是因为 titleLabel 的尺寸不正确

我想到的解决方法有三种

  • 继承 UIButton ,在子类的 layoutSubviews 中进行处理
  • 参考上面博客的作者在 Demo_ButtonImageTitleEdgeInsets 提供的,用 CGFloat labelWidth = [self.titleLabel.text sizeWithFont:self.titleLabel.font].width; 来实现,通过字符串计算出 titlelabel 的尺寸,来设置 titleEdgeInsets ,详情见代码段 1
  • 第三种通过 sizeToFit 和第二种思路是一样的,详情见代码段 2

代码段 1

@interface NSString(UIStringDrawing)

// Single line, no wrapping. Truncation based on the NSLineBreakMode.
- (CGSize)sizeWithFont:(UIFont *)font NS_DEPRECATED_IOS(2_0, 7_0, "Use -sizeWithAttributes:") __TVOS_PROHIBITED;

代码段 2

UIButton *b = [[UIButton alloc] initWithFrame:CGRectMake(10, 100, 100, 40)];
b.layer.borderColor = [UIColor blueColor].CGColor;
b.layer.borderWidth = 1;
[b setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
[b setTitle:@"测试" forState:UIControlStateNormal];
[b setImage:[UIImage imageNamed:@"arrow"] forState:UIControlStateNormal];
[self.view addSubview:b];

// 看这里 --- 
[b.titleLabel sizeToFit];

b.imageEdgeInsets = UIEdgeInsetsMake(0, b.titleLabel.frame.size.width, 0, -b.titleLabel.frame.size.width);
b.titleEdgeInsets = UIEdgeInsetsMake(0, -b.imageView.frame.size.width, 0, b.imageView.frame.size.width);

虽然位置不对,但尺寸已经是正确的了,也能实现合适的效果。

图标在下标题在上

根据上面的原理,应该可以比较简单的推算出的 imageEdgeInsetstitleEdgeInsets

要求: imageViewtitleLabel 都居中,且 imageVeiw 在上

两者都存在的情况下,原始的左右间距是( UIButton 的宽度 - 两者的宽度之和) * 0.5

imageViewX 要向右移动 ((UIButton 的宽度 - imageView 的宽度) - (UIButton 的宽度 - 两者的宽度之和)) * 0.5

titleLable 的宽度 * 0.5

阶段 1

    self.imageEdgeInsets = UIEdgeInsetsMake(0, self.titleLabel.frame.size.width * 0.5, 0, -self.titleLabel.frame.size.width * 0.5);
    self.titleEdgeInsets = UIEdgeInsetsMake(0, -self.imageView.frame.size.width * 0.5, 0, self.imageView.frame.size.width * 0.5);

两者都居中了,然后调整垂直方向

阶段 2

self.imageEdgeInsets = UIEdgeInsetsMake(-5, self.titleLabel.frame.size.width * 0.5, 5, -self.titleLabel.frame.size.width * 0.5);
self.titleEdgeInsets = UIEdgeInsetsMake(5, -self.imageView.frame.size.width * 0.5, -5, self.imageView.frame.size.width * 0.5);

效果有点丑...

说明: 这里忽略了 UIButton 放不下 titleLabel 或者用 xib 创建有 intrinsicSize 引用的问题。

假想的实现

CGFloat imageX = (CGRectGetWidth(self.imageView.frame)+ CGRectGetWidth(self.titleLabel.frame)) * 0.5 + (self.imageEdgeInsets.left - self.imageEdgeInsets.right) / 2;
CGFloat imageY = (CGRectGetHeight(self.frame) - self.imageView.image.size.height) * 0.5 + (self.imageEdgeInsets.top - self.imageEdgeInsets.bottom) / 2;
CGFloat imageW = self.imageView.image.size.width;
CGFloat imageH = self.imageView.image.size.height;
    
CGFloat titleX = (CGRectGetWidth(self.titleLabel.frame) + CGRectGetWidth(self.titleLabel.frame) * 0.5) + (self.titleEdgeInsets.left - self.titleEdgeInsets.right) / 2;
CGFloat titleY = (CGRectGetWidth(self.frame) - CGRectGetHeight(self.titleLabel.frame)) * 0.5 + (self.titleEdgeInsets.top - self.titleEdgeInsets.bottom) / 2;
CGFloat titleW = CGRectGetWidth(self.titleLabel.frame);
CGFloat titleH = CGRectGetHeight(self.titleLabel.frame);

总结

本文通过控制变量法😷 测试了 UIButtonimageEdgeInsetstitleEdgeInsets 属性的作用效果,发现两者是相互独立且只参考父视图 ( UIButton ) ,同时对实现图标在右标题在上,图标在上标题在下这两种样式提供了一点思路。

参考

  1. UIButton的titleEdgeInsets属性和imageEdgeInsets属性实现图片文字按要求排列
  2. Aligning text and image on UIButton with imageEdgeInsets and titleEdgeInsets
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容