前言:在移動 APP 的設計中,我們會經常看到同時帶有圖片和文字的按鈕,這些按鈕在 UI 設計師眼中,可能不值一提,但是在 iOS 開發中,由于 Apple 的 SDK 的局限性,實現起來卻并不那么愉快。
關鍵字:UIButton
,按鈕,圖文按鈕,圖片和文字的位置
相關源碼地址:ButtonLayoutDemo
目錄
- 需求
- 現實
- 問題
- 解決方案
- 小結
- 延伸閱讀
一、需求
根據以往的經驗來看,我們常見的圖文按鈕樣式一般是以下幾種的組合:
- 圖文布局
- 上圖下文
- 上文下圖
- 左圖右文
- 右圖左文
- 對齊方式
- 居左
- 居右
- 居中
- 居上
- 居下
- 對圖片做圓角處理
二、現實
然而,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];
效果如下:
三、問題
所以,我們首先要解決的問題是,怎樣才能輕松加愉快地實現:
- 圖文布局(可設置圖文間距)
- 對齊方式
我們所期望的是,只需要簡單地設置兩個屬性就能實現想要的效果:
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
繼承于 UIView
,UIView
是所有視圖控件的根類,UIView
中的跟布局相關的 API 有:
// 手動觸發 layout 的兩個方法,其中 - layoutIfNeeded 會強制 layout
- (void)setNeedsLayout;
- (void)layoutIfNeeded;
- (void)layoutSubviews; // layout 時該方法會被調用,調用 -layoutIfNeeded 方法會自動觸發這個方法
找來找去,就是系統提供給我們的就是這些工具了,看菜下飯吧。
方案一:設置 titleEdgeInsets
屬性和 imageEdgeInsets
屬性的值
如果你想要直接看最終實現的代碼,請戳這里UIButton+Layout.m。
titleEdgeInsets
:用來調整按鈕文字區域的位置和尺寸。
imageEdgeInsets
:用來調整按鈕圖片區域的位置和尺寸。
titleEdgeInsets
和 imageEdgeInsets
這兩個屬性都是 UIEdgeInsets
類型,UIEdgeInsets
類型有四個成員變量 top
、left
、bottom
、right
,分別表示上左下右四個方向的偏移量,正值代表往內縮進,也就是往按鈕中心靠攏,負值代表往外擴張,就是往按鈕邊緣貼近。
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。
知道以上兩個要點之后,我們就可以開始干活了,如果要想通過設置 titleEdgeInsets
和 imageEdgeInsets
來達到我們的要求,該怎么做呢?
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);
注意: 實際上,直接按照上面這么寫是不行的,因為設置 titleEdgeInsets
和 imageEdgeInsets
屬性時,button 的 titleLabel
和 imageView
的 frame 還沒有真正計算好,所以這個時候獲取到的 frame 是不準確的,要想拿到布局好的 titleLabel
和 imageView
的 frame ,我們需要先調用 - layoutIfNeeded
方法。
[button layoutIfNeeded];
// 然后設置 button 的 titleEdgeInsets 和 imageEdgeInsets
// ...
優雅的實現方式:直接在創建 button 的地方去調用 layoutIfNeeded
進行布局,再去計算 titleEdgeInsets
和 imageEdgeInsets
,并不是一個好的做法,比較推薦的做法是,寫一個 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; // 設置圖片區域的位置和大小
使用這兩個方法可以直接指定 titleLabel
和 imageView
的大小和位置,參數 contentRect
是由 -contentRectForBounds:
方法返回值決定的,如果該方法沒有被重寫,contentRect
就跟 bounds
的值是一樣的。
使用案例:
例如我們要實現一個上圖下文、整體靠頂部對齊、圖文間距 20pt 的圖案:
我們先自定義一個 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.titleLabel
和 self. imageView
,否則會出現無限遞歸,造成死循環。也就是說這里面的尺寸和位置計算,都是基于 contentRect
參數的獨立邏輯,所以一般只在我們知道圖片和文字的具體參數后才會這樣做,所以,這種方式使用起來并不靈活。
方案三:自定義一個 UIButton 的子類,重寫 layoutSubviews 計算位置
這個方案的啟發來源于騰訊 QMUI 團隊開源的 QMUIKit,其主要思想是,所有的 view 在布局時都會調用 -layoutSubviews
方法,你只要告訴我整體內容對齊方式是如何,圖文布局什么樣,圖文間距多大,我就可以在 -layoutSubviews
方法中幫你全部算好。
這種方式的好處在于可控性好,直接對 titleLabel
和 imageView
的 frame 進行操作,不用擔心系統實現會不會改動,其次,由于是直接操作 frame,計算起來就比較直觀簡單,不用像使用titleEdgeInsets
和 imageEdgeInsets
那樣把 titleLabel
和 imageView
挪來挪去。唯一不太好的地方在于計算量比較多,光計算布局就寫了差不多 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
的文字和圖片位置的方法,都有各自的優缺點,綜合起來看,方案三的自由度更高,可控性更好,也易于維護,使用起來更是輕松加愉快。