前言:在移動 APP 的設(shè)計中聚谁,我們會經(jīng)常看到同時帶有圖片和文字的按鈕滞诺,這些按鈕在 UI 設(shè)計師眼中形导,可能不值一提,但是在 iOS 開發(fā)中习霹,由于 Apple 的 SDK 的局限性朵耕,實現(xiàn)起來卻并不那么愉快。
關(guān)鍵字:UIButton
淋叶,按鈕阎曹,圖文按鈕,圖片和文字的位置
相關(guān)源碼地址:ButtonLayoutDemo
目錄
- 需求
- 現(xiàn)實
- 問題
- 解決方案
- 小結(jié)
- 延伸閱讀
一煞檩、需求
根據(jù)以往的經(jīng)驗來看处嫌,我們常見的圖文按鈕樣式一般是以下幾種的組合:
- 圖文布局
- 上圖下文
- 上文下圖
- 左圖右文
- 右圖左文
- 對齊方式
- 居左
- 居右
- 居中
- 居上
- 居下
- 對圖片做圓角處理
二、現(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];
效果如下:
三凝赛、問題
所以注暗,我們首先要解決的問題是厨剪,怎樣才能輕松加愉快地實現(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ū)域的位置和尺寸。
titleEdgeInsets
和 imageEdgeInsets
這兩個屬性都是 UIEdgeInsets
類型屯远,UIEdgeInsets
類型有四個成員變量 top
蔓姚、left
、bottom
慨丐、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è)置 titleEdgeInsets
和 imageEdgeInsets
來達(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è)置 titleEdgeInsets
和 imageEdgeInsets
屬性時装悲,button 的 titleLabel
和 imageView
的 frame 還沒有真正計算好昏鹃,所以這個時候獲取到的 frame 是不準(zhǔn)確的,要想拿到布局好的 titleLabel
和 imageView
的 frame 诀诊,我們需要先調(diào)用 - layoutIfNeeded
方法洞渤。
[button layoutIfNeeded];
// 然后設(shè)置 button 的 titleEdgeInsets 和 imageEdgeInsets
// ...
優(yōu)雅的實現(xiàn)方式:直接在創(chuàng)建 button 的地方去調(diào)用 layoutIfNeeded
進(jìn)行布局,再去計算 titleEdgeInsets
和 imageEdgeInsets
属瓣,并不是一個好的做法载迄,比較推薦的做法是,寫一個 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ū)域的位置和大小
使用這兩個方法可以直接指定 titleLabel
和 imageView
的大小和位置,參數(shù) contentRect
是由 -contentRectForBounds:
方法返回值決定的慈格,如果該方法沒有被重寫怠晴,contentRect
就跟 bounds
的值是一樣的。
使用案例:
例如我們要實現(xiàn)一個上圖下文浴捆、整體靠頂部對齊蒜田、圖文間距 20pt 的圖案:
我們先自定義一個 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.titleLabel
和 self. imageView
,否則會出現(xiàn)無限遞歸页眯,造成死循環(huán)梯捕。也就是說這里面的尺寸和位置計算,都是基于 contentRect
參數(shù)的獨立邏輯窝撵,所以一般只在我們知道圖片和文字的具體參數(shù)后才會這樣做傀顾,所以,這種方式使用起來并不靈活。
方案三:自定義一個 UIButton 的子類衩辟,重寫 layoutSubviews 計算位置
這個方案的啟發(fā)來源于騰訊 QMUI 團(tuán)隊開源的 QMUIKit丢习,其主要思想是逻澳,所有的 view 在布局時都會調(diào)用 -layoutSubviews
方法,你只要告訴我整體內(nèi)容對齊方式是如何趟薄,圖文布局什么樣瞎颗,圖文間距多大坐搔,我就可以在 -layoutSubviews
方法中幫你全部算好婉徘。
這種方式的好處在于可控性好茅逮,直接對 titleLabel
和 imageView
的 frame 進(jìn)行操作,不用擔(dān)心系統(tǒng)實現(xiàn)會不會改動判哥,其次,由于是直接操作 frame碉考,計算起來就比較直觀簡單塌计,不用像使用titleEdgeInsets
和 imageEdgeInsets
那樣把 titleLabel
和 imageView
挪來挪去。唯一不太好的地方在于計算量比較多侯谁,光計算布局就寫了差不多 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ù)拄查,使用起來更是輕松加愉快。