虽然前面也写了一篇有关Btn的titleEdgeInsets和imageEdgeInsets的介绍的,但是感觉可能存在问题,而且每次使用这两个属性的时候都没有那么的得心应手,有强迫症的我决定花点时间拿下这两个属性。下面,记录我的探索过程。(虽然现在的我已经知道如何使用了,但并不算正规,还请大神不吝赐教!!!)
首先,你得知道titleEdgeInsets和imageEdgeInsets的作用是用来移动btn两个子空间的排布的,且它们只是image和label相较于原来位置的偏移量,那什么是原来的位置呢?其实就是不设置Insets的那个状态。下面为不设置insets的状态。
originImage1.png
发现在默认情况下,imageView和titleLable是靠在一起并居中显示的。
其次,在同时有image和label且在默认情况下,这时候image的上左下边界是相对于button,右边是相对于label的;label的上右下边界是相对于button,左边是相对于image的。
最后
UIEdgeInsetsMake(CGFloat top, CGFloat left, CGFloat bottom, CGFloat right)
四个属性分别是对于控件的下边界,右边界,上边界,左边界。
结合上面知识举个例子:
btn.imageEdgeInsets = UIEdgeInsetsMake(0, 5, 0, -5);
这行代码的意思是对于image来说,下边界和上边界不动,右边界相对于label的左边界向右偏移5,左边界相对于btn的左边界向左偏移-5,也就是向右偏移5。所以可以想象image整体向右偏移5了。
再举个例子:
btn.titleEdgeInsets = UIEdgeInsetsMake(5, -5, -5, 5);
这行代码的意思是对于label来说,下边界相对于btn的下边界向下偏移5,右边界相对于btn的右边界向右偏移-5,上边界相对于btn的上边界偏移-5,左边界相对于image的右边界向左偏移5。所以可以想象label整体向左偏移5,向下偏移了5。
下面我来亲测是否真的是这样的。
首先,我先写了一个小demo来作为实验工具,界面如下:
image1.png
我放了一个slider和一个现实效果用的button。每次滑动slider都会改变titleEdgeInsets或者imageEdgeInsets。具体代码:(这里暂时写imageEdgeInsets好了)
UIButton *btn = [self.view viewWithTag:1];
btn.imageEdgeInsets = UIEdgeInsetsMake(0, self.slider1.value * 100, 0, -self.slider1.value * 100);
至于那个按钮,我给他定了50的高,120的宽,代码如下:
UIButton *btn1 = [UIButton new];
btn1.frame = CGRectMake(100, 300, 120, 50);
btn1.backgroundColor = [UIColor blueColor];
[btn1 setTitle:@"加好友" forState:UIControlStateNormal];
[btn1 setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
btn1.titleLabel.font = [UIFont systemFontOfSize:14];
[btn1 setImage:[UIImage imageNamed:@"btnArea_add@2x"] forState:UIControlStateNormal];
btn1.titleLabel.backgroundColor = [UIColor redColor];
btn1.imageView.backgroundColor = [UIColor yellowColor];
btn1.tag =1 ;
[self.view addSubview:btn1];
我们拿imageView的左右偏移来测试,滑动slider。
响应代码:
btn.imageEdgeInsets = UIEdgeInsetsMake(0, self.slider1.value * 100, 0, -self.slider1.value * 100);
效果图:
demo0.gif
再拿label的上下偏移来测试,滑动slider。
响应代码:
btn.titleEdgeInsets = UIEdgeInsetsMake(self.slider1.value * 100, 0, -self.slider1.value * 100, 0);
效果图:
demo1.gif
综上发现的确是理论说的那样的。
既然已经知道了这些参数的使用规律,那就成热打铁练习一下。
代码:
CGFloat btnW = btn1.bounds.size.width;
CGFloat btnH = btn1.bounds.size.height;
CGFloat imageW = btn1.imageView.bounds.size.width;
CGFloat imageH = btn1.imageView.bounds.size.height;
CGFloat labelW = btn1.titleLabel.bounds.size.width;
CGFloat labelH = btn1.titleLabel.bounds.size.height;
btn1.imageEdgeInsets = UIEdgeInsetsMake(0,0 + labelW,0,0 - labelW);
btn1.titleEdgeInsets = UIEdgeInsetsMake(0,0 - imageW,0,0 + imageW);
效果:
demo2.png
再次讲一下代码的意思吧。以image为例,他的右边界相当于label的左边界向右偏移labelW,左边界相当于btn的左边界向左偏移-labelW。上下不变。
demo3.png
然后label,他的右边界相对于btn的右边界向右偏移-imageW,左边界相对于image的右边界向左偏移imageW。
demo2.png
CGFloat imageOffsetX = (imageW + labelW) / 2 - imageW / 2;
CGFloat imageOffsetY = imageH / 2;
btn1.imageEdgeInsets = UIEdgeInsetsMake(-imageOffsetY, imageOffsetX, imageOffsetY, -imageOffsetX);
CGFloat labelOffsetX = (imageW + labelW / 2) - (imageW + labelW) / 2;
CGFloat labelOffsetY = labelH / 2;
btn1.titleEdgeInsets = UIEdgeInsetsMake(labelOffsetY, -labelOffsetX, -labelOffsetY, labelOffsetX);
虽然麻烦,但是不难。其余的自己去试喽~
但是,求问大神,当我的响应代码变为如下:
UIButton *btn = [self.view viewWithTag:1];
btn.imageEdgeInsets = UIEdgeInsetsMake(0, self.slider1.value * 100, 0, 0);
效果为什么变成这样:
demo4.gif
-----------------iOS button的imageEdgeInsets和titleEdgeInsets原理---------------------------------------------
demo地址: SPButton
最近我竟花了几天的时间去深入研究button,研究的过程当中,被imageEdgeInsets
和titleEdgeInsets
两个属性困惑甚久,我为此彻夜不眠,网上也查阅各种资料,可以说,对于这两个属性的解释,网上的答案满天飞,但是,没有一个人真正说出了它们的原理。
这是两个枚举,即整个内容的水平对齐方式和垂直对齐方式
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
截图中:
按钮
矩形区域,其bounds为:(0,0,200,100)
,为了便于研究,contentEdgeInset默认UIEdgeInsetsZero,即按钮的内容区域就是按钮的bounds;imageView
的frame为(50,25,50,50)
;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而言。
正确结论
imageEdgeInsets
和titleEdgeInsets
的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
从这里我们可以萌生一个思想
imageEdgeInsets
和titleEdgeInsets
不要去理解为将imageView和titleLabel进行平移,应该理解为将imageView和titleLabel的安全区域的各边进行偏移,偏移完成后,再联合contentHorizontalAlignment
和contentVerticalAlignment
属性进行整体对齐
我想大家在实现按钮图片位置在上、下、左、右的需求时,有不少人是通过重写按钮的imageRectForContentRect:
和titleRectForContentRect:
的,我个人也很推荐这种做法,重写layoutSubviews
也可以,但我并不推荐,可以说重写layoutSubviews可以实现你的需求,但是严重破坏了系统按钮,因为,系统按钮在layoutSubviews里面,当存在文字或者图片时,会先调用imageRectForContentRect:
和titleRectForContentRect:
这2个方法计算出imageRect和titleRect,然后将计算结果应用在imageView和titleLabel上,所以,如果你重写layoutSubviews,先super , 然后进行一系列自己的布局,这就会导致你使用button时,通过imageRectForContentRect:
和titleRectForContentRect:
这2个方法获取到的rect并非你在layoutSubviews里计算的结果,仍然是系统计算的结果,这就是破坏了原始按钮的方法
imageRectForContentRect:
和titleRectForContentRect:
的调用时机:
- 在第一次调用titleLabel和imageView的getter方法(懒加载)时,alloc init之前会调用一次(无论有无图片文字都会直接调),因此,在重写这2个方法时,在方法里面不要使用self.imageView和self.titleLabel,因为这2个控件是懒加载,如果在重写的这2个方法里是第一调用imageView和titleLabel的getter方法, 则会造成死循环
- 在layoutsSubviews中如果文字或图片不为空时会调用, 测试方式:在重写的这两个方法里调用setNeedsLayout(layutSubviews),发现会造成死循环
- 按钮的frame发生改变,设置文字图片、改动文字和图片、设置对齐方式,设置内容区域等时会调用,其实这些,系统是调用了layoutSubviews从而间接的去调用
imageRectForContentRect:
和titleRectForContentRect:
......
大家在实现按钮的图片在上、左、下、右的时候,最好要注意不要去破坏系统按钮,什么叫破坏呢?比如你实现完之后,要保证按钮的所有自带属性和方法依然生效,再比如:UIButton中的titleLabel和imageView是懒加载的,我们不要在实现自己需求的过程中去提前加载,这不符合按钮的规则
demo地址: SPButton
F728B222E090608891172DB207F7EF45.jpg
演示图.gif