iOS button的imageEdgeInsets和titleEdgeInsets原理

demo地址: SPButton

前言

最近我竟花了几天的时间去深入研究button,研究的过程当中,被imageEdgeInsetstitleEdgeInsets两个属性困惑甚久,我为此彻夜不眠,网上也查阅各种资料,可以说,对于这两个属性的解释,网上的答案满天飞,但是,没有一个人真正说出了它们的原理。

重要关联属性contentHorizontalAlignment和contentVerticalAlignment

这是两个枚举,即整个内容的水平对齐方式和垂直对齐方式

typedef NS_ENUM(NSInteger, UIControlContentHorizontalAlignment) {
    UIControlContentHorizontalAlignmentCenter = 0,
    UIControlContentHorizontalAlignmentLeft   = 1,
    UIControlContentHorizontalAlignmentRight  = 2,
    UIControlContentHorizontalAlignmentFill   = 3,
    UIControlContentHorizontalAlignmentLeading  API_AVAILABLE(ios(11.0), tvos(11.0)) = 4,
    UIControlContentHorizontalAlignmentTrailing API_AVAILABLE(ios(11.0), tvos(11.0)) = 5,
};

typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
    UIControlContentVerticalAlignmentCenter  = 0,
    UIControlContentVerticalAlignmentTop     = 1,
    UIControlContentVerticalAlignmentBottom  = 2,
    UIControlContentVerticalAlignmentFill    = 3,
};
// 默认:
 button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
 button.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;

其中UIControlContentHorizontalAlignmentLeading和UIControlContentHorizontalAlignmentTrailing为iOS11新增,在我们大中华地区,Leading就是Left,Trailing就是Right ,对于部分国家,他们的语言是从右往左写,这时Leading就是Right,Trailing就Left

正文

创建一个按钮,设置文字和图片,按钮的内容默认排布如图:为了便于理解,我给的titleLabel和imageView是等宽的


9EC15FFC4CC9871442AD43C376C72DF8.jpg

截图中:

  1. 黑色边框为按钮矩形区域,其bounds为:(0,0,200,100),为了便于研究,contentEdgeInset默认UIEdgeInsetsZero,即按钮的内容区域就是按钮的bounds;
  2. imageView的frame为(50,25,50,50)
  3. titleLabel的frame为(100,37.5,50,50)

现在,我设置

button.imageEdgeInsets = UIEdgeInsetsMake(0,50, 0,0);

经过上面的设置后,请大家猜想一下,图片的位置会在什么地方?
思考 1s、2s、3s、.......
大家心中差不多有想法了,图片的原x值为50,现在设置UIEdgeInsetsMake(0,50, 0,0),相当于整个图片向右平移50,那么现在图片的x值应该为100,大家想象的结果是不是这样的,如图:

2F4D2190781A35ADAC9188C5FC48F8CD.jpg

我要告诉大家,上面的结果是错的,正确的结果如图:
179A2A60E6C0637971FEB28BF5E1F50D.jpg

实际上,图片只向右平移了50的一半,即25,这是为什么?

网上错误结论:

对于imageView:其imageEdgeInsets的top,left,bottom是相对button的contentRect而言,right是相对titleLabel而言;
对于titleLabel:其titleEdgeInsets的top,right,bottom是相对button的contentRect而言,left是相对imageView而言。

正确结论

imageEdgeInsetstitleEdgeInsets的top,left,right, bottom都是相对button的contentRect而言,当contentEdgeInsets为UIEdgeInsetsZero时,button、imageView、titleLabel的安全区域均为button的bounds。

根据这个正确结论,当设置了button.imageEdgeInsets = UIEdgeInsetsMake(0,50, 0,0)时,那么imageView的安全区域就是如下图中的红色区域

669CA397468CDFA3318832E6E46F654D.jpg

图片的区域我们知道了,根据水平排列方式默认为UIControlContentHorizontalAlignmentCenter,图片应当在红色区域的中间位置,然而,我们要深刻明白:

重要的话说3遍

  • UIControlContentHorizontalAlignmentCenter的指的是内容(图片+文字)整体居中
  • UIControlContentHorizontalAlignmentCenter的指的是内容(图片+文字)整体居中
  • UIControlContentHorizontalAlignmentCenter的指的是内容(图片+文字)整体居中
    其余枚举值同理

因此,尽管titleLabel没有设置titleEdgeInsets,但是我们在对imageView进行某种对齐时,不应该只考虑imageView,应该将imageView+titleLabel这个整体作为考虑对象; 如图
F2C3BD086D79D226158CE915C5349A99.jpg

核心解释

上图中,imageView和蓝色的titleLabel作为一个整体,在红色区域内居中了,绿色的titleLabel只参与计算,由于我们没有设置titleLabel的titleEdgeInsets,所以最终titleLabel的位置依然保持不变。蓝色的titleLabel实际上是虚拟的,我只是告诉大家,系统进行对齐方式计算时,永远是把imageView+titleLabel这个整体作为计算对象,我们来计算一下,图片向右偏移25是怎么来的:
①红色区域的宽度为:200 - 50 = 150;
②图片+蓝色label的总宽度:50 + 50 = 100;
③图片的x值:(① - ②) / 2.0 =(150 - 100)/ 2.0 = 25;(除以2是因为居中对齐,如果是其余对齐就不用除以2)

我不知道我上面的表达够不够清楚,如果不清楚,那么我们来一次强化训练

强化训练

我们不再按照水平中心对齐,我们来一次左对齐

button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;

设置后如图


280C608ED3BFFE70D6865E80E99AAB6D.jpg

再设置

 button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 50);

大家想想经过上面那行代码之后,结果是什么呢?图片会向左偏移50的距离吗?如果按照网上的结论,图片的right是相对titleLabel而言,那么设置right为50图片必会向左偏移50。我要告诉大家,上面那行代码设置之后,不会产生任何变化,为什么?

原因很简单:上面那行代码的意思是,图片的安全区域为:在contentRect的基础上,原区域右边往左内缩50距离,即下图中的红色区域:
F3B155F5C3B22687C4C5AB809E993FC5.jpg

在这个红色区域当中,将imageView+(虚拟)titleLabel这个整体进行左对齐,大家明显能看到,现在就是左对齐的,所以设置right为50是不会有任何变化的,那么如果我们修改一下,设置
 button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 175);

上面那行代码的意思是,图片的安全区域为:在contentRect的基础上,原区域右边往左内缩175距离,即下图中的红色区域:
F6C4759589AC0ECDC7D607881F3B5A6E.jpg

在这个红色区域内,要把imageView+(虚拟)titleLabel这个整体进行左对齐,但是我们发现,红色区域的宽度容不下imageView+titleLabel这个整体,这个时候,系统先会把titleLabel的宽度压缩,如果压缩为0之后,发现连imageView都容不下,那么继续压缩imageView,直到宽度降为红色区域宽为止,titleLabel保持不动, 最终显示结果如图
F3B3E996C3F16FB079910B6F7E635DFB.jpg

再次训练

保持默认设置

button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
button.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;

再设置

button.imageEdgeInsets = UIEdgeInsetsMake(50, 0, 0, 0);

*上面那行代码的意思是,图片的安全区域为:在contentRect的基础上,原区域顶部向下内缩50距离,即下图中的红色区域:
F0893D4F8F8B780241E97EF8C9C8F541.jpg

在这个红色区域当中,要依然保证imageView+(虚拟)titleLabel这个整体进行垂直居中, 因此最终结果如图:
2102CCCB35A62B6562F0E927EB97BAA7.jpg

从这里我们可以萌生一个思想

imageEdgeInsetstitleEdgeInsets不要去理解为将imageView和titleLabel进行平移,应该理解为将imageView和titleLabel的安全区域的各边进行偏移,偏移完成后,再联合contentHorizontalAlignmentcontentVerticalAlignment属性进行整体对齐

我所知道的秘密

我想大家在实现按钮图片位置在上、下、左、右的需求时,有不少人是通过重写按钮的imageRectForContentRect:titleRectForContentRect:的,我个人也很推荐这种做法,重写layoutSubviews也可以,但我并不推荐,可以说重写layoutSubviews可以实现你的需求,但是严重破坏了系统按钮,因为,系统按钮在layoutSubviews里面,当存在文字或者图片时,会先调用imageRectForContentRect:titleRectForContentRect:这2个方法计算出imageRect和titleRect,然后将计算结果应用在imageView和titleLabel上,所以,如果你重写layoutSubviews,先super , 然后进行一系列自己的布局,这就会导致你使用button时,通过imageRectForContentRect:titleRectForContentRect:这2个方法获取到的rect并非你在layoutSubviews里计算的结果,仍然是系统计算的结果,这就是破坏了原始按钮的方法

  • imageRectForContentRect:titleRectForContentRect:的调用时机:
  1. 在第一次调用titleLabel和imageView的getter方法(懒加载)时,alloc init之前会调用一次(无论有无图片文字都会直接调),因此,在重写这2个方法时,在方法里面不要使用self.imageView和self.titleLabel,因为这2个控件是懒加载,如果在重写的这2个方法里是第一调用imageView和titleLabel的getter方法, 则会造成死循环
  2. 在layoutsSubviews中如果文字或图片不为空时会调用, 测试方式:在重写的这两个方法里调用setNeedsLayout(layutSubviews),发现会造成死循环
  3. 按钮的frame发生改变,设置文字图片、改动文字和图片、设置对齐方式,设置内容区域等时会调用,其实这些,系统是调用了layoutSubviews从而间接的去调用imageRectForContentRect:titleRectForContentRect:
    ......

建议

大家在实现按钮的图片在上、左、下、右的时候,最好要注意不要去破坏系统按钮,什么叫破坏呢?比如你实现完之后,要保证按钮的所有自带属性和方法依然生效,再比如:UIButton中的titleLabel和imageView是懒加载的,我们不要在实现自己需求的过程中去提前加载,这不符合按钮的规则

demo地址: SPButton

demo效果图

F728B222E090608891172DB207F7EF45.jpg

测试gif图

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

推荐阅读更多精彩内容