[iOS 開發] 如何調整 UIButton 中的元素(image 和 title)的布局?

前言:在移動 APP 的設計中,我們會經常看到同時帶有圖片和文字的按鈕,這些按鈕在 UI 設計師眼中,可能不值一提,但是在 iOS 開發中,由于 Apple 的 SDK 的局限性,實現起來卻并不那么愉快。

關鍵字UIButton,按鈕,圖文按鈕,圖片和文字的位置
相關源碼地址:ButtonLayoutDemo

目錄

  • 需求
  • 現實
  • 問題
  • 解決方案
  • 小結
  • 延伸閱讀

一、需求

根據以往的經驗來看,我們常見的圖文按鈕樣式一般是以下幾種的組合:

  • 圖文布局
    • 上圖下文
    • 上文下圖
    • 左圖右文
    • 右圖左文
  • 對齊方式
    • 居左
    • 居右
    • 居中
    • 居上
    • 居下
  • 對圖片做圓角處理
上圖下文(圖片帶圓角).png
上圖下文(整體靠底部對齊).png
上文下圖(整體靠底部對齊).png
系統默認支持的左圖右文(整體居中).png
左文右圖.png

二、現實

然而,Cocoa Touch 框架中的 UIButton 只支持左圖右文的布局方式,而且還不能直接設置圖文間距。

代碼如下:

UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(50, 50, 50, 30)];
[button setTitle:@"title" forState:UIControlStateNormal];
[button setImage:icon forState:UIControlStateNormal];
button.contentVerticalAlignment = UIControlContentHorizontalAlignmentCenter;
[self addSubview:button];

效果如下:


系統默認支持的左圖右文(整體居中).png

三、問題

所以,我們首先要解決的問題是,怎樣才能輕松加愉快地實現:

  • 圖文布局(可設置圖文間距)
  • 對齊方式

我們所期望的是,只需要簡單地設置兩個屬性就能實現想要的效果:


button.interTitleImageSpacing = 4;
button.imagePosition = UIButtonImagePositionRight;

四、解決方案

我們先來看看 UIButton 的 API ,發現跟內容布局相關的有這些:

@interface UIButton : UIControl
...
@property(nonatomic)          UIEdgeInsets contentEdgeInsets;  // 用來調整按鈕整體內容區域的位置和尺寸
@property(nonatomic)          UIEdgeInsets titleEdgeInsets;  // 用來調整按鈕文字區域的位置和尺寸
@property(nonatomic)          UIEdgeInsets imageEdgeInsets; // 用來調整按鈕圖片區域的位置和尺寸

- (CGRect)contentRectForBounds:(CGRect)bounds;  // 用來計算按鈕整體內容區域的大小和位置
- (CGRect)titleRectForContentRect:(CGRect)contentRect;  // 用來計算按鈕文字區域的大小和位置
- (CGRect)imageRectForContentRect:(CGRect)contentRect;  // 用來計算按鈕圖片區域的大小和位置
...
@end

UIButton 繼承于 UIControl,再來看看 UIControl 中的跟布局相關的 API:

@property(nonatomic) UIControlContentVerticalAlignment contentVerticalAlignment;     // 設置內容在豎直方向的對齊方式
@property(nonatomic) UIControlContentHorizontalAlignment contentHorizontalAlignment;  // 設置內容在水平方向的對齊方式

UIControl 繼承于 UIViewUIView 是所有視圖控件的根類,UIView 中的跟布局相關的 API 有:

// 手動觸發 layout 的兩個方法,其中 - layoutIfNeeded 會強制 layout
- (void)setNeedsLayout;
- (void)layoutIfNeeded;

- (void)layoutSubviews;   // layout 時該方法會被調用,調用 -layoutIfNeeded 方法會自動觸發這個方法

找來找去,就是系統提供給我們的就是這些工具了,看菜下飯吧。

方案一:設置 titleEdgeInsets 屬性和 imageEdgeInsets 屬性的值

如果你想要直接看最終實現的代碼,請戳這里UIButton+Layout.m

titleEdgeInsets :用來調整按鈕文字區域的位置和尺寸。
imageEdgeInsets:用來調整按鈕圖片區域的位置和尺寸。

titleEdgeInsetsimageEdgeInsets 這兩個屬性都是 UIEdgeInsets 類型,UIEdgeInsets 類型有四個成員變量 topleftbottomright,分別表示上左下右四個方向的偏移量,正值代表往內縮進,也就是往按鈕中心靠攏,負值代表往外擴張,就是往按鈕邊緣貼近。

typedef struct UIEdgeInsets {
    CGFloat top, left, bottom, right;  // specify amount to inset (positive) for each of the edges. values can be negative to 'outset'
} UIEdgeInsets;

具體怎么用呢?
要點:

  • 系統默認的布局是內容整體居中,圖片在左,文字在右,圖片和文字間距為 0。
  • 不論是 titleEdgeInsets,還是 imageEdgeInsets,只設置一個方向的偏移量 A 時,實際效果得到的偏移量是 A / 2。比如想通過
    button.titleEdgeInsets = UIEdgeInsetsMake(0, 2, 0, 0); 設置按鈕標題往右偏移 2 pt, 實際上得到的效果是按鈕文字只往右偏移了 1 pt。

知道以上兩個要點之后,我們就可以開始干活了,如果要想通過設置 titleEdgeInsetsimageEdgeInsets 來達到我們的要求,該怎么做呢?

1. 左圖右文

// 目標圖文間距
CGFloat interImageTitleSpacing = 5;
// 獲取默認的圖片文字間距
CGFloat originalSpacing = button.titleLabel.frame.origin.x - (button.imageView.frame.origin.x + button.imageView.frame.size.width);
// 調整文字的位置
button.titleEdgeInsets = UIEdgeInsetsMake(0,
                                        -(originalSpacing - interImageTitleSpacing),
                                        0,
                                        (originalSpacing - interImageTitleSpacing));

2. 左文右圖

    // 目標圖文間距
    CGFloat interImageTitleSpacing = 5;
    // 圖片右移
    button.imageEdgeInsets = UIEdgeInsetsMake(0,
                                              button.titleLabel.frame.size.width + interImageTitleSpacing,
                                              0,
                                              -(button.titleLabel.frame.size.width + interImageTitleSpacing));
    // 文字左移
    button.titleEdgeInsets = UIEdgeInsetsMake(0,
                                              -(button.titleLabel.frame.origin.x - button.imageView.frame.origin.x),
                                              0,
                                              button.titleLabel.frame.origin.x - button.imageView.frame.origin.x);

3.上圖下文

    // 目標圖文間距
    CGFloat interImageTitleSpacing = 5;

    // 圖片上移,右移
    button.imageEdgeInsets = UIEdgeInsetsMake(0,
                                            0,
                                            button.titleLabel.frame.size.height + interImageTitleSpacing,
                                            -(button.titleLabel.frame.size.width));
    
    // 文字下移,左移
    button.titleEdgeInsets = UIEdgeInsetsMake(button.imageView.frame.size.height + interImageTitleSpacing,
                                            -(button.imageView.frame.size.width),
                                            0,
                                            0);

4.上文下圖

    // 目標圖文間距
    CGFloat interImageTitleSpacing = 5;

    // 圖片下移,右移
    button.imageEdgeInsets = UIEdgeInsetsMake(button.titleLabel.frame.size.height + interImageTitleSpacing,
                                            0,
                                            0,
                                            -(button.titleLabel.frame.size.width));
    
    // 文字上移,左移
    button.titleEdgeInsets = UIEdgeInsetsMake(0,
                                            -(button.imageView.frame.size.width),
                                            button.imageView.frame.size.height + interImageTitleSpacing,
                                            0);

注意: 實際上,直接按照上面這么寫是不行的,因為設置 titleEdgeInsetsimageEdgeInsets 屬性時,button 的 titleLabelimageView 的 frame 還沒有真正計算好,所以這個時候獲取到的 frame 是不準確的,要想拿到布局好的 titleLabelimageView 的 frame ,我們需要先調用 - layoutIfNeeded 方法。

[button layoutIfNeeded];
//  然后設置 button 的 titleEdgeInsets 和 imageEdgeInsets 
// ... 

優雅的實現方式:直接在創建 button 的地方去調用 layoutIfNeeded 進行布局,再去計算 titleEdgeInsetsimageEdgeInsets,并不是一個好的做法,比較推薦的做法是,寫一個 category 或者 自定義一個 UIButton 的子類,來實現上面的計算,并提供圖片文字的布局樣式和圖文間距的接口。

接口應該長得像這樣:

typedef NS_ENUM(NSInteger, SCButtonLayoutStyle) {
    SCButtonLayoutStyleImageLeft,  
    SCButtonLayoutStyleImageRight,
    SCButtonLayoutStyleImageTop,
    SCButtonLayoutStyleImageBottom,
};

@interface UIButton (Layout)

- (void)sc_setLayoutStyle:(SCButtonLayoutStyle)style spacing:(CGFloat)spacing;

@end

使用起來應該像這樣:

    button.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;  // 豎直方向整體居上
    button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;  // 水平方向整體居中
    [button sc_setLayoutStyle:SCButtonLayoutStyleImageBottom spacing:20];  // 圖片在底部,圖文間距 20 pt

具體的代碼實現見 UIButton+Layout.m

方案二:自定義一個 UIButton 的子類,重寫以下兩個方法:

- (CGRect)titleRectForContentRect:(CGRect)contentRect;   // 設置文字區域的位置和大小
- (CGRect)imageRectForContentRect:(CGRect)contentRect;  // 設置圖片區域的位置和大小

使用這兩個方法可以直接指定 titleLabelimageView 的大小和位置,參數 contentRect 是由 -contentRectForBounds: 方法返回值決定的,如果該方法沒有被重寫,contentRect 就跟 bounds 的值是一樣的。

使用案例:
例如我們要實現一個上圖下文、整體靠頂部對齊、圖文間距 20pt 的圖案:

上圖下文(整體靠頂部對齊).png

我們先自定義一個 UIButton 的子類,實現 -titleRectForContentRect:-imageRectForContentRect: 方法:

@interface CustomButton : UIButton

@property (assign, nonatomic) CGFloat interTitleImageSpacing;  ///< 圖片文字間距


@end

@implementation CustomButton


- (CGRect)titleRectForContentRect:(CGRect)contentRect {
    
    CGSize titleSize = CGSizeMake(contentRect.size.width, 25);
    
    CGRect imageFrame = [self imageRectForContentRect:contentRect];
    
    return CGRectMake((contentRect.size.width - titleSize.width) * 0.5,
                      imageFrame.origin.y + imageFrame.size.height + self.interTitleImageSpacing,
                      titleSize.width,
                      titleSize.height);
}

- (CGRect)imageRectForContentRect:(CGRect)contentRect {
    
    CGSize imageSize = CGSizeMake(25, 24);
    
    return CGRectMake((contentRect.size.width - imageSize.width) * 0.5, 0, imageSize.width, imageSize.height);
}

@end

然后再在外面使用定義好的 CustomButton,然后就得到上圖中的效果了:

    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 75, 75)];
    [button setImage:[UIImage imageNamed:@"like"] forState:UIControlStateNormal];
    [button setTitle:@"title" forState:UIControlStateNormal];
    button.interTitleImageSpacing = 20;
    button.titleLabel.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:button];

注意:但是,在這兩個方法中不能使用 self.titleLabelself. imageView,否則會出現無限遞歸,造成死循環。也就是說這里面的尺寸和位置計算,都是基于 contentRect 參數的獨立邏輯,所以一般只在我們知道圖片和文字的具體參數后才會這樣做,所以,這種方式使用起來并不靈活。

方案三:自定義一個 UIButton 的子類,重寫 layoutSubviews 計算位置

這個方案的啟發來源于騰訊 QMUI 團隊開源的 QMUIKit,其主要思想是,所有的 view 在布局時都會調用 -layoutSubviews 方法,你只要告訴我整體內容對齊方式是如何,圖文布局什么樣,圖文間距多大,我就可以在 -layoutSubviews 方法中幫你全部算好。

這種方式的好處在于可控性好,直接對 titleLabelimageView 的 frame 進行操作,不用擔心系統實現會不會改動,其次,由于是直接操作 frame,計算起來就比較直觀簡單,不用像使用titleEdgeInsetsimageEdgeInsets 那樣把 titleLabelimageView 挪來挪去。唯一不太好的地方在于計算量比較多,光計算布局就寫了差不多 150 行代碼。

因為 QMUIKit 中的 QMUIButton 太過于龐雜,其中有很多我們并不需要的功能,維護起來也復雜,所以我針對我們自己項目的需求實現了一個更簡潔的 SCCustomButton,主要支持以下功能:

  • 設置圖文布局方式
  • 設置圖文間距
  • 設置圖片圓角大小
  • 設置內容整體對齊方式

這是 SCCustomButton 提供的接口:

/// 圖片和文字的相對位置
typedef NS_ENUM(NSInteger, SCCustomButtonImagePosition) {
    SCCustomButtonImagePositionTop,     // 圖片在文字頂部
    SCCustomButtonImagePositionLeft,    // 圖片在文字左側
    SCCustomButtonImagePositionBottom,  // 圖片在文字底部
    SCCustomButtonImagePositionRight    // 圖片在文字右側
};

/**
 自定義按鈕,可控制圖片文字間距
 
 使用方法:
 @code
     SCCustomButton *button = [[SCCustomButton alloc] initWithFrame:CGRectMake(50, 50, 50, 30)];
     button.imagePosition = SCCustomButtonImagePositionLeft;  // 圖文布局方式
     button.interTitleImageSpacing = 5;                       // 圖文間距
     button.imageCornerRadius = 15;                           // 圖片圓角半徑
     button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;  // 內容對齊方式
     [self addSubview:button];
 @endcode
 */
@interface SCCustomButton : UIButton

@property (assign, nonatomic) CGFloat interTitleImageSpacing;  ///< 圖片文字間距
@property (assign, nonatomic) SCCustomButtonImagePosition imagePosition;     ///< 圖片和文字的相對位置
@property (assign, nonatomic) CGFloat imageCornerRadius;                     ///< 圖片圓角半徑

@end

使用起來也非常簡單,正好符合我們期望的效果:

SCCustomButton *button = [[SCCustomButton alloc] initWithFrame:CGRectMake(50, 50, 50, 30)];
     button.imagePosition = SCCustomButtonImagePositionLeft;  // 圖文布局方式
     button.interTitleImageSpacing = 5;                       // 圖文間距
     button.imageCornerRadius = 15;                           // 圖片圓角半徑
     button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;  // 內容對齊方式
     [self addSubview:button];

另辟蹊徑:自定義一個 UIView 或者 UIControl 的子類,實現所要求的樣式

當然也可以不使用 UIButton,自己去實現一個繼承于 UIView 或者 UIControl 的子類,這是完全可以滿足我們所要求的樣式的,但是這樣就需要自己添加和管理 imageView 和 label,并實現一些 UIButton 的功能(比如點擊按鈕時的高亮效果),顯然是比前面提到的幾種方式更復雜,成本也更高。

五、小結

以上幾種調整 UIButton 的文字和圖片位置的方法,都有各自的優缺點,綜合起來看,方案三的自由度更高,可控性更好,也易于維護,使用起來更是輕松加愉快。

六、延伸閱讀

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容