淺析UIButton的imageEdgeInsets與titleEdgeInsets

導(dǎo)語

系統(tǒng)的 UIButton 默認狀態(tài)下的樣式是圖標在左標題在右,但有時候可能需要不同的排版。當然可以通過繼承添加子視圖來實現(xiàn)需求,但本文打算通過理解 UIButton 自帶的 imageEdgeInsetstitleEdgeInsets 屬性實現(xiàn)該功能。

主要內(nèi)容包含以下兩點:

  • 淺析 imageEdgeInsetstitleEdgeInsets 的屬性的原理 [個人觀點]
  • 簡單實現(xiàn)圖標在右標題在左,圖標在上標題在下。

環(huán)境

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.

使用此屬性可調(dià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)于 UIEdgeInsetsMaketop, left, bottom, right正數(shù)表明更靠近按鈕的中心,負數(shù)表示更靠近按鈕的邊緣,默認為 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 即可實現(xiàn)整體居中,因此圖標的 X 就是 (100 - 47) * 0.5 = 26.5

測試 4

只調(diào)整 titleEdgeInsets [注釋 2 4]

標題的位置信息如下

測試 5

只調(diào)整 imageEdgeInsets [注釋 1 3]

圖標的位置信息如下

問題 2

比較測試 4 與測試 1 會發(fā)現(xiàn)

  • titleEdgeInsetsright 設(shè)置了 10 , X 卻向左偏移了 5 ?

同樣對比 測試 5 與測試 2 會發(fā)現(xiàn)

  • imageEdgeInsetsright 設(shè)置了 10 , X 卻向左偏移了 5

為什么會有這種減半的現(xiàn)象呢?

尋找 & 分析

在搜索后,這篇 博客對我有所啟發(fā)

回答 1

它們只是image和button相較于原來位置的偏移量,那什么是原來的位置呢?就是這個

沒有設(shè)置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 拆成兩個屬性,兩者的位置應(yīng)該不互相依賴才對,即使依賴,也應(yīng)該依賴 UIButton 這個父視圖比較合適。

測試 6

在標題和圖標同時存在的情況下,調(diào)整 titleEdgeInsets [注釋 4]

標題

圖標


測試 7

在標題和圖標同時存在的情況下,調(diào)整 imageEdgeInsets [注釋 3]

標題


圖標

測試 8

在標題和圖標同時存在的情況下,調(diào)整 imageEdgeInsetstitleEdgeInsets [不注釋]

標題


圖標


小結(jié)

將測試 3 和 測試 6 或 測試 3 和測試 7 對比會發(fā)現(xiàn)即使在標題和圖標同時存在的情況下,單獨調(diào)整 imageEdgeInsetstitleEdgeInsets 都只會對對應(yīng)的視圖的位置產(chǎn)生影響,而且影響同樣是 減半 的。而通過將測試 3 和 測試 8 比較, titleEdgeInsetsimageEdgeInsets 同時作用的情況下也是一樣的。

佐證

上面的參考博客中提到 Aligning text and image on UIButton with imageEdgeInsets and titleEdgeInsetsStackOverflow 上關(guān)于這個問題的一個討論。里面有這樣一段話,對我有所啟發(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。

官方的注釋也許正如上面這段話所表達的,只是在告訴我們 imageEdgeInsets/titleEdgeInsets 其實只是描述了父視圖(UIButton)與它們各自視圖的間距。

回答 2

在理解了 imageEdgeInsets/titleEdgeInsets 的獨立性后,我嘗試用自己的話來說明為什么會存在"減半"的效果。

比如下面的代碼

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

在水平方向上 titleLabel 左側(cè)偏移量為 0titleLabel 右側(cè)偏移量為 10 ,但是偏移量 X 的變化量。

X 左移 10 那么實際上的偏移屬性值應(yīng)該為 (0,-10,0,10) :

左偏移量(相對于 button 左側(cè)),title 從原始向目標是往邊緣方向,所以是負值(參考本文最初的文檔理解),偏移量為 10,右偏移量(相對 button 右側(cè)),從原始到目標是往中間方向,所以是正值。

因此上面的代碼表明 titleLabel 相對 UIButton 左側(cè)不改變,右側(cè)改變 10 ,應(yīng)該是做不到的,左側(cè)間距增加1,右側(cè)間距必然會減少1。會被等價轉(zhuǎn)換為

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

個人推測

如何轉(zhuǎn)換?

參考 iOS 的坐標系,水平向右為 X 軸正方向 ,垂直向下為 Y 正方向,要根據(jù) titleEdgeInsets / imageEdgeInsets 去計算 titleimage 的坐標,可以對 UIEdgeInsets 結(jié)構(gòu)體的四個成員( top , left , bottom , right ) 進行處理 (在負方向留正偏移量即是往正方向偏移)。

公式如下

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

直白點的話: 負間距(left,top)更靠近,正間距(left,top)更遠離,原始狀態(tài)就是 UIEdgeInsets 全為 0 即你不去操作 titleEdgeInsets / imageEdgeInsets 時。

個人推測

實踐

實踐是檢驗真理的唯一標準;不管黑貓白貓,能抓老鼠的就是好??...

圖標在右標題在左

根據(jù)上面的猜想,要實現(xiàn)圖標與標題的位置交換,很簡單: imageView 左側(cè)相對于 UIButton 向中央移動了 titleLabel 的寬度,記為 titleLabel.w,右側(cè)相對于 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);
}

標題


圖標


但是如果用直接去設(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:@"測試" 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);

你會發(fā)現(xiàn)效果并不如意,原因是因為 titleLabel 的尺寸不正確

我想到的解決方法有三種

  • 繼承 UIButton ,在子類的 layoutSubviews 中進行處理
  • 參考上面博客的作者在 Demo_ButtonImageTitleEdgeInsets 提供的,用 CGFloat labelWidth = [self.titleLabel.text sizeWithFont:self.titleLabel.font].width; 來實現(xiàn),通過字符串計算出 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:@"測試" 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);

雖然位置不對,但尺寸已經(jīng)是正確的了,也能實現(xiàn)合適的效果。

圖標在下標題在上

根據(jù)上面的原理,應(yīng)該可以比較簡單的推算出的 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);

兩者都居中了,然后調(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);

效果有點丑...

說明: 這里忽略了 UIButton 放不下 titleLabel 或者用 xib 創(chuàng)建有 intrinsicSize 引用的問題。

假想的實現(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é)

本文通過控制變量法?? 測試了 UIButtonimageEdgeInsetstitleEdgeInsets 屬性的作用效果,發(fā)現(xiàn)兩者是相互獨立且只參考父視圖 ( UIButton ) ,同時對實現(xiàn)圖標在右標題在上,圖標在上標題在下這兩種樣式提供了一點思路。

參考

  1. UIButton的titleEdgeInsets屬性和imageEdgeInsets屬性實現(xiàn)圖片文字按要求排列
  2. Aligning text and image on UIButton with imageEdgeInsets and titleEdgeInsets
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容