導(dǎo)語
系統(tǒng)的 UIButton 默認(rèn)狀態(tài)下的樣式是圖標(biāo)在左標(biāo)題在右缎谷,但有時(shí)候可能需要不同的排版莉钙。當(dāng)然可以通過繼承添加子視圖來實(shí)現(xiàn)需求,但本文打算通過理解 UIButton 自帶的 imageEdgeInsets 和 titleEdgeInsets 屬性實(shí)現(xiàn)該功能阳距。
主要內(nèi)容包含以下兩點(diǎn):
- 淺析 imageEdgeInsets 和 titleEdgeInsets 的屬性的原理 [個(gè)人觀點(diǎn)]
- 簡(jiǎn)單實(shí)現(xiàn)圖標(biāo)在右標(biāo)題在左,圖標(biāo)在上標(biāo)題在下腐巢。
環(huán)境
macOS Sierra 10.12.4
Xcode 8.3.2
iPhone 6S (10.1.1)
流程
先從蘋果官方對(duì)該方法的注釋入手
The inset or outset margins for the rectangle around the button’s title text.
使用此屬性可調(diào)整按鈕標(biāo)題的有效繪圖矩形的大小并重新定位。(來自 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.
關(guān)于 UIEdgeInsetsMake 的 top, left, bottom, right ,正數(shù)表明更靠近按鈕的中心嘲驾,負(fù)數(shù)表示更靠近按鈕的邊緣淌哟,默認(rèn)為 UIEdgeInsetsZero 。
問題 1
- margins 是邊距的含義辽故,那原始的位置在哪?
測(cè)試代碼
#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:@"測(cè)試" 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];
測(cè)試 1
只有標(biāo)題 [注釋 2 3 4]
標(biāo)題的位置如下
標(biāo)題居中
測(cè)試 2
只有圖標(biāo) [注釋 1 3 4]
圖標(biāo)位置信息如下
圖標(biāo)居中
測(cè)試 3
標(biāo)題 + 圖標(biāo) [注釋 3 4]
圖標(biāo)
標(biāo)題
為了維持居中就要將標(biāo)題和圖標(biāo)看做一個(gè)整體即寬度和為 37 + 10 = 47 徒仓,用 UIButton 的寬度來減去總寬度再乘以 0.5 即可實(shí)現(xiàn)整體居中,因此圖標(biāo)的 X 就是 (100 - 47) * 0.5 = 26.5
測(cè)試 4
只調(diào)整 titleEdgeInsets [注釋 2 4]
標(biāo)題的位置信息如下
測(cè)試 5
只調(diào)整 imageEdgeInsets [注釋 1 3]
圖標(biāo)的位置信息如下
問題 2
比較測(cè)試 4 與測(cè)試 1 會(huì)發(fā)現(xiàn)
- titleEdgeInsets 的 right 設(shè)置了 10 , X 卻向左偏移了 5 ?
同樣對(duì)比 測(cè)試 5 與測(cè)試 2 會(huì)發(fā)現(xiàn)
- imageEdgeInsets 的 right 設(shè)置了 10 , X 卻向左偏移了 5
為什么會(huì)有這種減半的現(xiàn)象呢?
尋找 & 分析
在搜索后誊垢,這篇 博客對(duì)我有所啟發(fā)
回答 1
它們只是image和button相較于原來位置的偏移量掉弛,那什么是原來的位置呢?就是這個(gè)
沒有設(shè)置edgeInset時(shí)候的位置了喂走。
如要要image在右邊殃饿,label在左邊,那image的左邊相對(duì)于button的左邊右移了labelWidth的距離芋肠,image的右邊相對(duì)于label的左邊右移了labelWidth的距離
所以乎芳,self.oneButton.imageEdgeInsets = UIEdgeInsetsMake(0, labelWidth, 0, -labelWidth);
為什么是負(fù)值呢?因?yàn)檫@是contentInset帖池,是偏移量奈惑,不是距離
同樣的,label
的右邊相對(duì)于button
的右邊左移了imageWith
的距離睡汹,label
的左邊相對(duì)于image
的右邊左移了imageWith
的距離
所以self.oneButton.titleEdgeInsets = UIEdgeInsetsMake(0, -imageWith, 0, imageWith)
; 這樣就完成image在右邊肴甸,label在左邊的效果了。
但是對(duì)下面的前置知識(shí)點(diǎn)囚巴,感覺有些疑惑
前置知識(shí)點(diǎn):titleEdgeInsets是title相對(duì)于其上下左右的inset原在,跟tableView的contentInset是類似的,如果只有title彤叉,那它上下左右都是相對(duì)于button的庶柿,image也是一樣;
如果同時(shí)有image和label秽浇,那這時(shí)候image的上左下是相對(duì)于button澳泵,右邊是相對(duì)于label的;title的上右下是相對(duì)于button兼呵,左邊是相對(duì)于image的。
我認(rèn)為雖然兩者都在 UIButton 中腊敲,但 Apple 既然將 imageEdgeInsets 和 titleEdgeInsets 拆成兩個(gè)屬性击喂,兩者的位置應(yīng)該不互相依賴才對(duì),即使依賴碰辅,也應(yīng)該依賴 UIButton 這個(gè)父視圖比較合適懂昂。
測(cè)試 6
在標(biāo)題和圖標(biāo)同時(shí)存在的情況下,調(diào)整 titleEdgeInsets [注釋 4]
標(biāo)題
圖標(biāo)
測(cè)試 7
在標(biāo)題和圖標(biāo)同時(shí)存在的情況下没宾,調(diào)整 imageEdgeInsets [注釋 3]
標(biāo)題
圖標(biāo)
測(cè)試 8
在標(biāo)題和圖標(biāo)同時(shí)存在的情況下凌彬,調(diào)整 imageEdgeInsets 和 titleEdgeInsets [不注釋]
標(biāo)題
圖標(biāo)
小結(jié)
將測(cè)試 3 和 測(cè)試 6 或 測(cè)試 3 和測(cè)試 7 對(duì)比會(huì)發(fā)現(xiàn)即使在標(biāo)題和圖標(biāo)同時(shí)存在的情況下沸柔,單獨(dú)調(diào)整 imageEdgeInsets 或 titleEdgeInsets 都只會(huì)對(duì)對(duì)應(yīng)的視圖的位置產(chǎn)生影響,而且影響同樣是 減半 的铲敛。而通過將測(cè)試 3 和 測(cè)試 8 比較褐澎, titleEdgeInsets 和 imageEdgeInsets 同時(shí)作用的情況下也是一樣的。
佐證
上面的參考博客中提到 Aligning text and image on UIButton with imageEdgeInsets and titleEdgeInsets 是 StackOverflow 上關(guān)于這個(gè)問題的一個(gè)討論伐蒋。里面有這樣一段話工三,對(duì)我有所啟發(fā)
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。
官方的注釋也許正如上面這段話所表達(dá)的先鱼,只是在告訴我們 imageEdgeInsets/titleEdgeInsets 其實(shí)只是描述了父視圖(UIButton)與它們各自視圖的間距俭正。
回答 2
在理解了 imageEdgeInsets/titleEdgeInsets 的獨(dú)立性后,我嘗試用自己的話來說明為什么會(huì)存在"減半"的效果焙畔。
比如下面的代碼
self.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 10);
在水平方向上 titleLabel 左側(cè)偏移量為 0掸读,titleLabel 右側(cè)偏移量為 10 ,但是偏移量 ≠ X 的變化量宏多。
X 左移 10 那么實(shí)際上的偏移屬性值應(yīng)該為 (0,-10,0,10) :
左偏移量(相對(duì)于 button 左側(cè))儿惫,title 從原始向目標(biāo)是往邊緣方向,所以是負(fù)值(參考本文最初的文檔理解)绷落,偏移量為 10姥闪,右偏移量(相對(duì) button 右側(cè)),從原始到目標(biāo)是往中間方向砌烁,所以是正值筐喳。
因此上面的代碼表明 titleLabel 相對(duì) UIButton 左側(cè)不改變,右側(cè)改變 10 函喉,應(yīng)該是做不到的避归,左側(cè)間距增加1,右側(cè)間距必然會(huì)減少1管呵。會(huì)被等價(jià)轉(zhuǎn)換為
self.titleEdgeInsets = UIEdgeInsetsMake(0, -5, 0, 5);
個(gè)人推測(cè)
如何轉(zhuǎn)換?
參考 iOS 的坐標(biāo)系梳毙,水平向右為 X 軸正方向 ,垂直向下為 Y 正方向,要根據(jù) titleEdgeInsets / imageEdgeInsets 去計(jì)算 title 和 image 的坐標(biāo)捐下,可以對(duì) UIEdgeInsets 結(jié)構(gòu)體的四個(gè)成員( top , left , bottom , right )
進(jìn)行處理 (在負(fù)方向留正偏移量即是往正方向偏移)账锹。
公式如下
- 水平方向上 X 的偏移量:
(left + (-1) * right) / 2
- 垂直方向上 Y 的偏移量:
(top + (-1) * bottom) / 2
直白點(diǎn)的話: 負(fù)間距(left,top
)更靠近,正間距(left,top
)更遠(yuǎn)離坷襟,原始狀態(tài)就是 UIEdgeInsets 全為 0 即你不去操作 titleEdgeInsets / imageEdgeInsets 時(shí)奸柬。
個(gè)人推測(cè)
實(shí)踐
實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn);不管黑貓白貓婴程,能抓老鼠的就是好??...
圖標(biāo)在右標(biāo)題在左
根據(jù)上面的猜想廓奕,要實(shí)現(xiàn)圖標(biāo)與標(biāo)題的位置交換,很簡(jiǎn)單: imageView 左側(cè)相對(duì)于 UIButton 向中央移動(dòng)了 titleLabel 的寬度,記為 titleLabel.w桌粉,右側(cè)相對(duì)于 UIButton 向邊緣同樣移動(dòng)了 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);
}
標(biāo)題
圖標(biāo)
但是如果用直接去設(shè)置 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:@"測(cè)試" 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);
你會(huì)發(fā)現(xiàn)效果并不如意蒸绩,原因是因?yàn)?titleLabel 的尺寸不正確
我想到的解決方法有三種
- 繼承 UIButton ,在子類的 layoutSubviews 中進(jìn)行處理
- 參考上面博客的作者在 Demo_ButtonImageTitleEdgeInsets 提供的铃肯,用
CGFloat labelWidth = [self.titleLabel.text sizeWithFont:self.titleLabel.font].width;
來實(shí)現(xiàn)患亿,通過字符串計(jì)算出 titlelabel 的尺寸,來設(shè)置 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:@"測(cè)試" 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);
雖然位置不對(duì),但尺寸已經(jīng)是正確的了宴胧,也能實(shí)現(xiàn)合適的效果漱抓。
圖標(biāo)在下標(biāo)題在上
根據(jù)上面的原理,應(yīng)該可以比較簡(jiǎn)單的推算出的 imageEdgeInsets 和 titleEdgeInsets
要求: imageView 和 titleLabel 都居中恕齐,且 imageVeiw 在上
兩者都存在的情況下乞娄,原始的左右間距是( UIButton 的寬度 - 兩者的寬度之和) * 0.5
,
imageView 的 X 要向右移動(dòng) ((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);
兩者都居中了显歧,然后調(diào)整垂直方向
階段 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);
效果有點(diǎn)丑...
說明: 這里忽略了 UIButton 放不下 titleLabel 或者用 xib 創(chuàng)建有 intrinsicSize 引用的問題仪或。
假想的實(shí)現(xiàn)
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);
總結(jié)
本文通過控制變量法?? 測(cè)試了 UIButton 的 imageEdgeInsets 和 titleEdgeInsets 屬性的作用效果,發(fā)現(xiàn)兩者是相互獨(dú)立且只參考父視圖 ( UIButton ) 士骤,同時(shí)對(duì)實(shí)現(xiàn)圖標(biāo)在右標(biāo)題在上范删,圖標(biāo)在上標(biāo)題在下這兩種樣式提供了一點(diǎn)思路。