UIButton的官方文檔
https://developer.apple.com/reference/uikit/uibutton
1、創建
When adding a button to your interface, perform the following steps:
- Set the type of the button at creation time.
- Supply a title string or image; size the button appropriately for your content.
- Connect one or more action methods to the button.
- Set up Auto Layout rules to govern the size and position of the button in your interface.
- Provide accessibility information and localized strings.
// 創建按鈕的函數
+ (instancetype)buttonWithType:(UIButtonType)buttonType;
// 按鈕類型
typedef NS_ENUM(NSInteger, UIButtonType)
{
UIButtonTypeCustom = 0, // no button type
UIButtonTypeSystem NS_ENUM_AVAILABLE_IOS(7_0), // standard system button
UIButtonTypeDetailDisclosure,
UIButtonTypeInfoLight,
UIButtonTypeInfoDark,
UIButtonTypeContactAdd,
UIButtonTypeRoundedRect = UIButtonTypeSystem, // Deprecated, use UIButtonTypeSystem instead
};
// 例子
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(100, 100, 100, 50);
btn.backgroundColor = [UIColor grayColor];
btn.titleLabel.text = @"設置標題無效"; // 注意,這樣設置標題是無效的,后文有解釋
[btn setTitle:@"標題" forState:UIControlStateNormal];
[self.view addSubview:btn];
需要注意的是,*After creating a button, you cannot change its type. * 所以最好不用init方法創建按鈕。如果按鈕的類型不是UIButtonTypeCustom,那么設置按鈕的圖片或某些屬性是沒有效果的。比如在故事板拖一個按鈕出來,默認是UIButtonTypeSystem 類型,然后通過代碼修改按鈕的圖片就沒有效果。
2、響應
用戶點擊按鈕產生事件時,按鈕不直接處理,而是采用 Target-Action 設計模式,通知taget調用action來處理。
要禁止交互,設置按鈕的 userInteractionEnabled 或者 enabled = NO,只要有一個為NO就會禁止交互。這兩個的區別是,enabled 是 UIControl 的屬性,userInteractionEnabled 是 UIView 的屬性。enabled = NO 不僅禁止交互而且會把按鈕的狀態設置為 UIControlStateDisabled。
// 調用這個函數把button和action方法連接起來
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
// UIControlEvents,這里只列出touch相關的
typedef NS_OPTIONS(NSUInteger, UIControlEvents) {
UIControlEventTouchDown = 1 << 0, // on all touch downs
UIControlEventTouchDownRepeat = 1 << 1, // on multiple touchdowns (tap count > 1)
UIControlEventTouchDragInside = 1 << 2,
UIControlEventTouchDragOutside = 1 << 3,
UIControlEventTouchDragEnter = 1 << 4,
UIControlEventTouchDragExit = 1 << 5,
UIControlEventTouchUpInside = 1 << 6,
UIControlEventTouchUpOutside = 1 << 7,
UIControlEventTouchCancel = 1 << 8,
// ...其他的省略了
}
// 注意了,action方法的三種格式
- (IBAction)doSomething;
- (IBAction)doSomething:(id)sender;
- (IBAction)doSomething:(id)sender forEvent:(UIEvent*)event;
// 例子
// 點擊按鈕發生UIControlEventTouchUpInside事件時,car會調用run方法
[btn addTarget:car action:@selector(run) forControlEvents:UIControlEventTouchUpInside];
// 汽車類的action
- (void)run {
NSLog(@"汽車啟動");
}
UIControlEvents 是描述控件事件類型的常量,查看官方文檔點這里。
為了方便理解各種事件,這里有例子代碼,還有幾篇寫的不錯的文章。
經過代碼實驗,結論如下:
手指按下,發生touch down事件。如果手指移動,會連續發生touch drag事件。如果手指抬起來,發生touch up事件。如果手指沒抬起來,有電話打進來或者發生別的狀況,會發生touch cancel事件。
手指按下多次,比如雙擊,會發生TouchDownRepeat 事件。手指在不超過黃色區域內移動,會連續發生TouchDragInside 事件,超過黃色區域會連續發生TouchDragOutside 事件。手指移動超出黃色區域,會發生TouchDragExit 事件,反之發生TouchDragEnter 事件。手指在黃色區域內抬起來,會發生TouchUpInside 事件,黃色區域外會發生TouchUpOutside 事件。
如下圖,灰色是按鈕,單個手指按下按鈕然后移動到A點再抬起來,發生的事件如下:
UIControlEventTouchDown,多次UIControlEventTouchDragInside,UIControlEventTouchDragExit,多次UIControlEventTouchDragOutside,UIControlEventTouchUpOutside。
黃色的區域是多大呢?官方文檔說的是 *UIControlEventTouchUpOutside, A touch-up event in the control where the finger is outside the bounds of the control. * 圖片里面,按鈕的size是(100, 50),黃色區域是(240, 200),按鈕的center和黃色區域的center相同,基本上黃色區域的大小就是UIControlEventTouchDragExit 發生的邊界了。至于為啥是這么大,我也沒弄清楚。這個好像并不重要。
3、外觀
按鈕狀態
官方文檔摘取:Buttons have five states that define their appearance: default, highlighted, focused, selected, and disabled. A disabled button is normally dimmed and does not display a highlight when tapped. In the highlighted state, an image-based button draws a highlight on top of the default image if no custom image is provided.
// 按鈕的五種狀態
typedef NS_OPTIONS(NSUInteger, UIControlState) {
UIControlStateNormal = 0,
UIControlStateHighlighted = 1 << 0, // used when UIControl isHighlighted is set
UIControlStateDisabled = 1 << 1,
UIControlStateSelected = 1 << 2, // flag usable by app (see below)
UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable only when the screen supports focus
... //此處有省略
};
如下圖,灰色的按鈕默認狀態是 UIControlStateNormal,手指按下會變成 UIControlStateHighlighted,手指不抬起來,移出按鈕但是不超出黃色區域狀態不變,否則變成 UIControlStateNormal,再移進去又變成 UIControlStateHighlighted,手指抬起來變回 UIControlStateNormal。黃色區域的大小跟按鈕的bounds有關,具體不太清楚。
設置按鈕的 enabled = NO ,狀態會變成 UIControlStateDisabled,但是設置 userInteractionEnabled = NO就不會。在此狀態下,按鈕不會響應用戶操作。官方文檔:*An enabled control is capable of responding to user interactions, whereas a disabled control ignores touch events and may draw itself differently. *
要禁止交互,設置按鈕的 userInteractionEnabled 或者 enabled = NO,只要有一個為NO就會禁止交互。這兩個的區別是,enabled 是 UIControl 的屬性,userInteractionEnabled 是 UIView 的屬性。enabled = NO 不僅禁止交互而且會把按鈕的狀態設置為 UIControlStateDisabled。
設置按鈕的 selected = YES,狀態就會變成 UIControlStateSelected。在此狀態下,手指按下按鈕會變成 UIControlStateNormal(不是應該會變成 UIControlStateHighlighted 狀態嗎?可是代碼運行結果是變成 normal 狀態),移出黃色區域會變回 UIControlStateSelected,好神奇。
綜述,默認狀態下,手指按下按鈕會變成高亮狀態,其他狀態需要設置對應屬性來切換。
按鈕內容
title, image, background, tintColor, edegs inset,
按鈕可以同時顯示一張圖片、標題和背景圖片。默認圖片在左邊,文字在右邊。視圖層次從上往下是標題、圖片、背景圖片。
注意了,如果設置的圖片大小超出按鈕的寬度,label就會被擠出按鈕右邊,顯示不了,系統會設置label的高度為0。這個坑了我好長時間,心痛。
// 栗子
// 設置標題
[btn setTitle:@"標題" forState:UIControlStateNormal];
btn.titleLabel.text = @"標題無效"; // 無效
btn.titleLabel.textColor = [UIColor blackColor]; // 無效
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; // 有效
// 設置圖片
UIImage *avatar = [UIImage imageNamed:@"小小女孩"];
btn.imageView.image = avatar; // 無效
[btn setImage:avatar forState:UIControlStateNormal];
btn.imageView.contentMode = UIViewContentModeScaleAspectFit; // 設置圖片的填充方式
// 設置背景圖片
UIImage *backgroundImage = [UIImage imageNamed:@"葉子"];
[btn setBackgroundImage:backgroundImage forState:UIControlStateNormal];
注意,按鈕的 title、attributedTitle、image、titleColor、titleShadowColor 都要通過 setXXX: forState: 函數來設置,不能直接對 titleLabel 或 imageView 進行設置,因為會被按鈕重新設置為對應狀態下的值。按鈕里面應該有數組或字典保存相應狀態下的值。
內容的位置和大小
Frame
更新:獲取titleLabel的size的最簡單的方法
// 由標題長度決定,label是否被按鈕裁剪不影響該值
CGSize titleSize = btn.titleLabel.intrinsicContentSize;
// 定義在UIView.h中
// The natural size for the receiving view, considering only properties of the view itself.
@property(nonatomic, readonly) CGSize intrinsicContentSize NS_AVAILABLE_IOS(6_0);
先討論titleLabel的frame。
CGRect btnFrame = btn.frame;
CGRect labelFrame = btn.titleLabel.frame;
// 注意!titleRectForContentRect:起作用的前提是要訪問一次titleLabel,設置背景色或者設置frame都可以,原因不明。
CGRect titleRect = [btn titleRectForContentRect:btn.frame];
這三句代碼在創建按鈕的時候斷點調試,結果如圖labelFrame-1所示
在點擊按鈕的處理函數中斷點調試,結果如圖labelFrame-2所示
可以看到,labelFrame在創建和點擊時不同,創建時是假的,點擊時才是真的。而 titleRect.x = btnFrame.x + labelFrame.x,因此titleRect反映的是titleLabel在按鈕的直接父視圖中的frame,而不是在按鈕中的frame。
因此在創建按鈕時,titleLabel的大小,只能通過titleRectForContentRect:獲取。
需要注意的是,如果按鈕的寬度小于圖片和標題寬度之和,標題會被裁剪,有可能標題的寬高都會被設置為0。如果要把title放到圖片下方,那么按鈕的寬度就不夠了,label會被裁剪,只能通過NSString 的 sizeWithAttributes: 方法獲取大概寬度了,或者創建一個臨時的足夠寬度的按鈕來獲取標題寬度。(這里可以通過UILabel的 intrinsicContentSize 來獲取)。
按鈕的imageView的大小可以直接獲取。不建議通過imageView.image.size來獲取,因為圖片可能比按鈕大。
CGRect imageViewFrame = btn.imageView.frame;
CGRect imageRect = [btn imageRectForContentRect:btn.frame];
這兩句代碼,在創建按鈕和點擊按鈕時,調試結果一樣。
和titleRect一樣,imageRect反映的是imageView在按鈕的父視圖中的frame,而不是在按鈕中的frame。
EdgeInsets
imageView和titleLabel的frame直接修改是沒有效果的,只能通過按鈕的titleEdgeInsets、imageEdgeInsets、contentEdgeInsets進行修改。
// 屬性
@property(nonatomic) UIEdgeInsets contentEdgeInsets UI_APPEARANCE_SELECTOR;
@property(nonatomic) UIEdgeInsets titleEdgeInsets; // default is UIEdgeInsetsZero
@property(nonatomic) UIEdgeInsets imageEdgeInsets; // default is UIEdgeInsetsZero
// 定義
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;
// 例子。4個參數,上左下右,逆時針。
// 這里左邊界增大10,如果是左對齊,就會向右平移10。如果是-10會向左平移10。
UIEdgeInsets insets = UIEdgeInsetsMake(0, 10, 0, 0);
什么是edgeInsets呢?文檔對 titleEdgeInsets 的說明是:
The inset or outset margins for the rectangle around the button’s title text.
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.
This property is used only for positioning the title during layout. The button does not use this property to determine intrinsicContentSize and sizeThatFits:.
大意是正數會靠近center,負數會遠離。
我理解為邊界的厚度,就像手機邊框。假設有個iPhone平放在桌子上,左對齊,手機到桌子的距離和手機屏幕到桌子的距離是不等的,此時手機屏幕就像按鈕的title label,屏幕到桌子的距離就像label到按鈕的距離。如果屏幕大小不變,手機到桌子的距離不變,但是又要屏幕顯示的內容往右邊移,就只能增加手機邊框了。同理,要讓按鈕的標題往右移動,不改變按鈕的大小和按鈕的位置,就只能通過設置edgeInsets來修改title label的邊框大小了。
邊界看不見,不影響frame的大小,但是參與布局。
Alignment
按鈕的 contentVerticalAlignment、contentHorizontalAlignment 屬性影響對齊方式,對上述三個edgeInsets的效果也有影響。
// 垂直對齊方式
typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
UIControlContentVerticalAlignmentCenter = 0,
UIControlContentVerticalAlignmentTop = 1,
UIControlContentVerticalAlignmentBottom = 2,
UIControlContentVerticalAlignmentFill = 3,
};
// 水平對齊方式
typedef NS_ENUM(NSInteger, UIControlContentHorizontalAlignment) {
UIControlContentHorizontalAlignmentCenter = 0,
UIControlContentHorizontalAlignmentLeft = 1,
UIControlContentHorizontalAlignmentRight = 2,
UIControlContentHorizontalAlignmentFill = 3,
};
// 垂直對齊方式為頂部對齊
btn.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;
// 水平對齊方式為左對齊
btn.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
// 這里左邊界增大10。如果是左對齊,就會向右平移10。如果是-10會向左平移10。
// 如果是劇中對齊,只會向右平移5。
btn.titleEdgeInsets = UIEdgeInsetsMake(0, 10, 0, 0);
Demo
按鈕默認圖片在左,標題在右邊。如果要交換圖片和標題的位置,可以這樣寫:
CGSize labelSize = btn.titleLabel.intrinsicContentSize;
CGSize imageSize = btn.imageView.frame.size;
btn.imageEdgeInsets = UIEdgeInsetsMake(0, labelSize.width, 0, -labelSize.width);
btn.titleEdgeInsets = UIEdgeInsetsMake(0, -imageSize.width, 0, imageSize.width);
如果要標題在圖片下方,可以這樣寫:
// 注意!按鈕比圖片小很多的時候,效果不可預測,原因不明。
- (void)createUpDownButton {
// 創建按鈕
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(100, 100, 82, 100);
btn.backgroundColor = [UIColor grayColor];
[btn setImage:[UIImage imageNamed:@"小小女孩"] forState:UIControlStateNormal];
[btn setTitle:@"標題" forState:UIControlStateNormal];
[self.view addSubview:btn];
// 設置對齊方式
btn.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;
btn.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
// 獲取圖片和標題大小
CGSize labelSize = btn.titleLabel.intrinsicContentSize;
CGSize imageSize = btn.imageView.frame.size;
// 往下平移的距離
float top = imageSize.height + 5;
// 往左平移的距離
float left = (imageSize.width + labelSize.width) / 2;
// 設置標題偏移量,上左下右。要往左移,所以是負的。
btn.titleEdgeInsets = UIEdgeInsetsMake(top, -left, 0, 0);
// 設置內容偏移量,上左下右,對圖片和標題都起作用。
btn.contentEdgeInsets = UIEdgeInsetsMake(10, 10, 0, 0);
}
例子看似簡單,在不知道要通過intrinsicContentSize獲取標題寬度、圖片大于按鈕是特殊情況、對齊方式會影響偏移量效果的時候,被坑得懷疑人生。
設置左對齊和頂部對齊實現起來最簡便,計算簡單,坑最少。比如按鈕寬度不夠,標題顯示為...,可以設置為左對齊,減小左邊界往左移(理解為在左邊給它更多空間顯示)就可以正常顯示了。
計算往左的偏移量,思路是兩個控件的center.x重疊,所以偏移量就是長度之和的一半。
默認標題在圖片右邊,所以要往左移。減小左邊界和增大右邊界都能使標題左移,區別是右邊界增大到一定程度就不會左移了,而是壓縮標題成...了。左對齊的話,左邊界減小量就是平移量,居中對齊的話減小量要乘以2,或者左邊界減小的同時右邊界增大。
遇到圖片比按鈕大的,只能通過故事板慢慢調了,不清楚蘋果內部是如何計算的。
4、總結
創建按鈕要調用類方法,按鈕創建之后不能修改類型,UIButtonTypeCustom類型才能自定義圖片。
按鈕采用 Target-Action 設計模式,action方法有三種格式,理解按鈕的各鐘事件觸發操作,順序是touch down-drag-up或者cancel。
要禁止按鈕交互,設置enabled = NO,按鈕的狀態會變成 UIControlStateDisabled,而設置 userInteractionEnabled則不會影響狀態。
按鈕狀態有5種,默認、高亮、禁用、選中、UIControlStateFocused(好像和apple tv的按鈕相關)。默認和高亮狀態可以通過交互改變,禁用和選中狀態要通過代碼改變(enabled、selected屬性)。
按鈕的標題和圖片,跟狀態相關的屬性要通過*setXXX: forState: *函數來設置,不能直接對 titleLabel 或 imageView 進行設置。如果圖片大于按鈕,標題的寬高會變為0。
在創建按鈕時,通過UILabel的intrinsicContentSize獲取標題的原始寬度。訪問過按鈕的titleLabel后,可以通過UIButton的titleRectForContentRect:獲取標題在按鈕的直接父視圖中的frame,而不是在按鈕中的frame。按鈕的imageView的大小就是imageView.frame.size。
按鈕的標題和圖片的大小和位置只能通過titleEdgeInsets、imageEdgeInsets、contentEdgeInsets進行修改。正數靠近中心(壓縮空間),負數遠離中心(擴展空間)。
按鈕的 contentVerticalAlignment、contentHorizontalAlignment 屬性影響對齊方式,對上述三個edgeInsets的效果也有影響。