[iOS 開發(fā)] 如何調(diào)整 UIButton 中的元素(image 和 title)的布局碉渡?

前言:在移動 APP 的設(shè)計中聚谁,我們會經(jīng)常看到同時帶有圖片和文字的按鈕滞诺,這些按鈕在 UI 設(shè)計師眼中形导,可能不值一提,但是在 iOS 開發(fā)中习霹,由于 Apple 的 SDK 的局限性朵耕,實現(xiàn)起來卻并不那么愉快。

關(guān)鍵字UIButton淋叶,按鈕阎曹,圖文按鈕,圖片和文字的位置
相關(guān)源碼地址:ButtonLayoutDemo

目錄

  • 需求
  • 現(xiàn)實
  • 問題
  • 解決方案
  • 小結(jié)
  • 延伸閱讀

一煞檩、需求

根據(jù)以往的經(jīng)驗來看处嫌,我們常見的圖文按鈕樣式一般是以下幾種的組合:

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

二、現(xiàn)實

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

代碼如下:

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];

效果如下:


系統(tǒng)默認(rèn)支持的左圖右文(整體居中).png

三凝赛、問題

所以注暗,我們首先要解決的問題是厨剪,怎樣才能輕松加愉快地實現(xiàn):

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

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


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

四友存、解決方案

我們先來看看 UIButton 的 API ,發(fā)現(xiàn)跟內(nèi)容布局相關(guān)的有這些:

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

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

UIButton 繼承于 UIControl陶衅,再來看看 UIControl 中的跟布局相關(guān)的 API:

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

UIControl 繼承于 UIView屡立,UIView 是所有視圖控件的根類,UIView 中的跟布局相關(guān)的 API 有:

// 手動觸發(fā) layout 的兩個方法搀军,其中 - layoutIfNeeded 會強(qiáng)制 layout
- (void)setNeedsLayout;
- (void)layoutIfNeeded;

- (void)layoutSubviews;   // layout 時該方法會被調(diào)用膨俐,調(diào)用 -layoutIfNeeded 方法會自動觸發(fā)這個方法

找來找去,就是系統(tǒng)提供給我們的就是這些工具了罩句,看菜下飯吧焚刺。

方案一:設(shè)置 titleEdgeInsets 屬性和 imageEdgeInsets 屬性的值

如果你想要直接看最終實現(xiàn)的代碼,請戳這里UIButton+Layout.m门烂。

titleEdgeInsets :用來調(diào)整按鈕文字區(qū)域的位置和尺寸乳愉。
imageEdgeInsets:用來調(diào)整按鈕圖片區(qū)域的位置和尺寸。

titleEdgeInsetsimageEdgeInsets 這兩個屬性都是 UIEdgeInsets 類型屯远,UIEdgeInsets 類型有四個成員變量 top蔓姚、leftbottom慨丐、right坡脐,分別表示上左下右四個方向的偏移量,正值代表往內(nèi)縮進(jìn)房揭,也就是往按鈕中心靠攏备闲,負(fù)值代表往外擴(kuò)張,就是往按鈕邊緣貼近捅暴。

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;

具體怎么用呢恬砂?
要點:

  • 系統(tǒng)默認(rèn)的布局是內(nèi)容整體居中,圖片在左伶唯,文字在右觉既,圖片和文字間距為 0。
  • 不論是 titleEdgeInsets乳幸,還是 imageEdgeInsets瞪讼,只設(shè)置一個方向的偏移量 A 時,實際效果得到的偏移量是 A / 2粹断。比如想通過
    button.titleEdgeInsets = UIEdgeInsetsMake(0, 2, 0, 0); 設(shè)置按鈕標(biāo)題往右偏移 2 pt符欠, 實際上得到的效果是按鈕文字只往右偏移了 1 pt。

知道以上兩個要點之后瓶埋,我們就可以開始干活了希柿,如果要想通過設(shè)置 titleEdgeInsetsimageEdgeInsets 來達(dá)到我們的要求诊沪,該怎么做呢?

1. 左圖右文

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

2. 左文右圖

    // 目標(biāo)圖文間距
    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.上圖下文

    // 目標(biāo)圖文間距
    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.上文下圖

    // 目標(biāo)圖文間距
    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);

注意: 實際上渐裸,直接按照上面這么寫是不行的,因為設(shè)置 titleEdgeInsetsimageEdgeInsets 屬性時装悲,button 的 titleLabelimageView 的 frame 還沒有真正計算好昏鹃,所以這個時候獲取到的 frame 是不準(zhǔn)確的,要想拿到布局好的 titleLabelimageView 的 frame 诀诊,我們需要先調(diào)用 - layoutIfNeeded 方法洞渤。

[button layoutIfNeeded];
//  然后設(shè)置 button 的 titleEdgeInsets 和 imageEdgeInsets 
// ... 

優(yōu)雅的實現(xiàn)方式:直接在創(chuàng)建 button 的地方去調(diào)用 layoutIfNeeded 進(jìn)行布局,再去計算 titleEdgeInsetsimageEdgeInsets属瓣,并不是一個好的做法载迄,比較推薦的做法是,寫一個 category 或者 自定義一個 UIButton 的子類抡蛙,來實現(xiàn)上面的計算宪巨,并提供圖片文字的布局樣式和圖文間距的接口。

接口應(yīng)該長得像這樣:

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

@interface UIButton (Layout)

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

@end

使用起來應(yīng)該像這樣:

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

具體的代碼實現(xiàn)見 UIButton+Layout.m

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

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

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

使用案例:
例如我們要實現(xiàn)一個上圖下文浴捆、整體靠頂部對齊蒜田、圖文間距 20pt 的圖案:

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

我們先自定義一個 UIButton 的子類,實現(xiàn) -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,否則會出現(xiàn)無限遞歸页眯,造成死循環(huán)梯捕。也就是說這里面的尺寸和位置計算,都是基于 contentRect 參數(shù)的獨立邏輯窝撵,所以一般只在我們知道圖片和文字的具體參數(shù)后才會這樣做傀顾,所以,這種方式使用起來并不靈活。

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

這個方案的啟發(fā)來源于騰訊 QMUI 團(tuán)隊開源的 QMUIKit丢习,其主要思想是逻澳,所有的 view 在布局時都會調(diào)用 -layoutSubviews 方法,你只要告訴我整體內(nèi)容對齊方式是如何趟薄,圖文布局什么樣瞎颗,圖文間距多大坐搔,我就可以在 -layoutSubviews 方法中幫你全部算好婉徘。

這種方式的好處在于可控性好茅逮,直接對 titleLabelimageView 的 frame 進(jìn)行操作,不用擔(dān)心系統(tǒng)實現(xiàn)會不會改動判哥,其次,由于是直接操作 frame碉考,計算起來就比較直觀簡單塌计,不用像使用titleEdgeInsetsimageEdgeInsets 那樣把 titleLabelimageView 挪來挪去。唯一不太好的地方在于計算量比較多侯谁,光計算布局就寫了差不多 150 行代碼锌仅。

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

  • 設(shè)置圖文布局方式
  • 設(shè)置圖文間距
  • 設(shè)置圖片圓角大小
  • 設(shè)置內(nèi)容整體對齊方式

這是 SCCustomButton 提供的接口:

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

/**
 自定義按鈕惨撇,可控制圖片文字間距
 
 使用方法:
 @code
     SCCustomButton *button = [[SCCustomButton alloc] initWithFrame:CGRectMake(50, 50, 50, 30)];
     button.imagePosition = SCCustomButtonImagePositionLeft;  // 圖文布局方式
     button.interTitleImageSpacing = 5;                       // 圖文間距
     button.imageCornerRadius = 15;                           // 圖片圓角半徑
     button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;  // 內(nèi)容對齊方式
     [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;  // 內(nèi)容對齊方式
     [self addSubview:button];

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

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

五翻诉、小結(jié)

以上幾種調(diào)整 UIButton 的文字和圖片位置的方法,都有各自的優(yōu)缺點捌刮,綜合起來看碰煌,方案三的自由度更高,可控性更好绅作,也易于維護(hù)拄查,使用起來更是輕松加愉快。

六棚蓄、延伸閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末堕扶,一起剝皮案震驚了整個濱河市碍脏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌稍算,老刑警劉巖典尾,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異糊探,居然都是意外死亡钾埂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門科平,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褥紫,“玉大人,你說我怎么就攤上這事瞪慧∷杩迹” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵弃酌,是天一觀的道長氨菇。 經(jīng)常有香客問我,道長妓湘,這世上最難降的妖魔是什么查蓉? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮榜贴,結(jié)果婚禮上豌研,老公的妹妹穿的比我還像新娘。我一直安慰自己唬党,他們只是感情好聂沙,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著初嘹,像睡著了一般及汉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上屯烦,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天坷随,我揣著相機(jī)與錄音,去河邊找鬼驻龟。 笑死温眉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的翁狐。 我是一名探鬼主播类溢,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了闯冷?” 一聲冷哼從身側(cè)響起砂心,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蛇耀,沒想到半個月后辩诞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡纺涤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年译暂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撩炊。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡外永,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拧咳,到底是詐尸還是另有隱情伯顶,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布呛踊,位于F島的核電站,受9級特大地震影響啦撮,放射性物質(zhì)發(fā)生泄漏谭网。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一赃春、第九天 我趴在偏房一處隱蔽的房頂上張望愉择。 院中可真熱鬧,春花似錦织中、人聲如沸锥涕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽层坠。三九已至,卻和暖如春刁笙,著一層夾襖步出監(jiān)牢的瞬間破花,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工疲吸, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留座每,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓摘悴,卻偏偏與公主長得像峭梳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蹂喻,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

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