UIButton的官方文檔
https://developer.apple.com/reference/uikit/uibutton
1、創(chuàng)建
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.
// 創(chuàng)建按鈕的函數(shù)
+ (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 = @"設(shè)置標(biāo)題無效"; // 注意,這樣設(shè)置標(biāo)題是無效的,后文有解釋
[btn setTitle:@"標(biāo)題" forState:UIControlStateNormal];
[self.view addSubview:btn];
需要注意的是,*After creating a button, you cannot change its type. * 所以最好不用init方法創(chuàng)建按鈕。如果按鈕的類型不是UIButtonTypeCustom,那么設(shè)置按鈕的圖片或某些屬性是沒有效果的。比如在故事板拖一個(gè)按鈕出來,默認(rèn)是UIButtonTypeSystem 類型,然后通過代碼修改按鈕的圖片就沒有效果。
2、響應(yīng)
用戶點(diǎn)擊按鈕產(chǎn)生事件時(shí),按鈕不直接處理,而是采用 Target-Action 設(shè)計(jì)模式,通知taget調(diào)用action來處理。
要禁止交互,設(shè)置按鈕的 userInteractionEnabled 或者 enabled = NO,只要有一個(gè)為NO就會(huì)禁止交互。這兩個(gè)的區(qū)別是,enabled 是 UIControl 的屬性,userInteractionEnabled 是 UIView 的屬性。enabled = NO 不僅禁止交互而且會(huì)把按鈕的狀態(tài)設(shè)置為 UIControlStateDisabled。
// 調(diào)用這個(gè)函數(shù)把button和action方法連接起來
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
// UIControlEvents,這里只列出touch相關(guān)的
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;
// 例子
// 點(diǎn)擊按鈕發(fā)生UIControlEventTouchUpInside事件時(shí),car會(huì)調(diào)用run方法
[btn addTarget:car action:@selector(run) forControlEvents:UIControlEventTouchUpInside];
// 汽車類的action
- (void)run {
NSLog(@"汽車啟動(dòng)");
}
UIControlEvents 是描述控件事件類型的常量,查看官方文檔點(diǎn)這里。
為了方便理解各種事件,這里有例子代碼,還有幾篇寫的不錯(cuò)的文章。
經(jīng)過代碼實(shí)驗(yàn),結(jié)論如下:
手指按下,發(fā)生touch down事件。如果手指移動(dòng),會(huì)連續(xù)發(fā)生touch drag事件。如果手指抬起來,發(fā)生touch up事件。如果手指沒抬起來,有電話打進(jìn)來或者發(fā)生別的狀況,會(huì)發(fā)生touch cancel事件。
手指按下多次,比如雙擊,會(huì)發(fā)生TouchDownRepeat 事件。手指在不超過黃色區(qū)域內(nèi)移動(dòng),會(huì)連續(xù)發(fā)生TouchDragInside 事件,超過黃色區(qū)域會(huì)連續(xù)發(fā)生TouchDragOutside 事件。手指移動(dòng)超出黃色區(qū)域,會(huì)發(fā)生TouchDragExit 事件,反之發(fā)生TouchDragEnter 事件。手指在黃色區(qū)域內(nèi)抬起來,會(huì)發(fā)生TouchUpInside 事件,黃色區(qū)域外會(huì)發(fā)生TouchUpOutside 事件。
如下圖,灰色是按鈕,單個(gè)手指按下按鈕然后移動(dòng)到A點(diǎn)再抬起來,發(fā)生的事件如下:
UIControlEventTouchDown,多次UIControlEventTouchDragInside,UIControlEventTouchDragExit,多次UIControlEventTouchDragOutside,UIControlEventTouchUpOutside。
黃色的區(qū)域是多大呢?官方文檔說的是 *UIControlEventTouchUpOutside, A touch-up event in the control where the finger is outside the bounds of the control. * 圖片里面,按鈕的size是(100, 50),黃色區(qū)域是(240, 200),按鈕的center和黃色區(qū)域的center相同,基本上黃色區(qū)域的大小就是UIControlEventTouchDragExit 發(fā)生的邊界了。至于為啥是這么大,我也沒弄清楚。這個(gè)好像并不重要。
3、外觀
按鈕狀態(tài)
官方文檔摘?。?em>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.
// 按鈕的五種狀態(tài)
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
... //此處有省略
};
如下圖,灰色的按鈕默認(rèn)狀態(tài)是 UIControlStateNormal,手指按下會(huì)變成 UIControlStateHighlighted,手指不抬起來,移出按鈕但是不超出黃色區(qū)域狀態(tài)不變,否則變成 UIControlStateNormal,再移進(jìn)去又變成 UIControlStateHighlighted,手指抬起來變回 UIControlStateNormal。黃色區(qū)域的大小跟按鈕的bounds有關(guān),具體不太清楚。
設(shè)置按鈕的 enabled = NO ,狀態(tài)會(huì)變成 UIControlStateDisabled,但是設(shè)置 userInteractionEnabled = NO就不會(huì)。在此狀態(tài)下,按鈕不會(huì)響應(yīng)用戶操作。官方文檔:*An enabled control is capable of responding to user interactions, whereas a disabled control ignores touch events and may draw itself differently. *
要禁止交互,設(shè)置按鈕的 userInteractionEnabled 或者 enabled = NO,只要有一個(gè)為NO就會(huì)禁止交互。這兩個(gè)的區(qū)別是,enabled 是 UIControl 的屬性,userInteractionEnabled 是 UIView 的屬性。enabled = NO 不僅禁止交互而且會(huì)把按鈕的狀態(tài)設(shè)置為 UIControlStateDisabled。
設(shè)置按鈕的 selected = YES,狀態(tài)就會(huì)變成 UIControlStateSelected。在此狀態(tài)下,手指按下按鈕會(huì)變成 UIControlStateNormal(不是應(yīng)該會(huì)變成 UIControlStateHighlighted 狀態(tài)嗎?可是代碼運(yùn)行結(jié)果是變成 normal 狀態(tài)),移出黃色區(qū)域會(huì)變回 UIControlStateSelected,好神奇。
綜述,默認(rèn)狀態(tài)下,手指按下按鈕會(huì)變成高亮狀態(tài),其他狀態(tài)需要設(shè)置對(duì)應(yīng)屬性來切換。
按鈕內(nèi)容
title, image, background, tintColor, edegs inset,
按鈕可以同時(shí)顯示一張圖片、標(biāo)題和背景圖片。默認(rèn)圖片在左邊,文字在右邊。視圖層次從上往下是標(biāo)題、圖片、背景圖片。
注意了,如果設(shè)置的圖片大小超出按鈕的寬度,label就會(huì)被擠出按鈕右邊,顯示不了,系統(tǒng)會(huì)設(shè)置label的高度為0。這個(gè)坑了我好長時(shí)間,心痛。
// 栗子
// 設(shè)置標(biāo)題
[btn setTitle:@"標(biāo)題" forState:UIControlStateNormal];
btn.titleLabel.text = @"標(biāo)題無效"; // 無效
btn.titleLabel.textColor = [UIColor blackColor]; // 無效
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; // 有效
// 設(shè)置圖片
UIImage *avatar = [UIImage imageNamed:@"小小女孩"];
btn.imageView.image = avatar; // 無效
[btn setImage:avatar forState:UIControlStateNormal];
btn.imageView.contentMode = UIViewContentModeScaleAspectFit; // 設(shè)置圖片的填充方式
// 設(shè)置背景圖片
UIImage *backgroundImage = [UIImage imageNamed:@"葉子"];
[btn setBackgroundImage:backgroundImage forState:UIControlStateNormal];
注意,按鈕的 title、attributedTitle、image、titleColor、titleShadowColor 都要通過 setXXX: forState: 函數(shù)來設(shè)置,不能直接對(duì) titleLabel 或 imageView 進(jìn)行設(shè)置,因?yàn)闀?huì)被按鈕重新設(shè)置為對(duì)應(yīng)狀態(tài)下的值。按鈕里面應(yīng)該有數(shù)組或字典保存相應(yīng)狀態(tài)下的值。
內(nèi)容的位置和大小
Frame
更新:獲取titleLabel的size的最簡單的方法
// 由標(biāo)題長度決定,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,設(shè)置背景色或者設(shè)置frame都可以,原因不明。
CGRect titleRect = [btn titleRectForContentRect:btn.frame];
這三句代碼在創(chuàng)建按鈕的時(shí)候斷點(diǎn)調(diào)試,結(jié)果如圖labelFrame-1所示
在點(diǎn)擊按鈕的處理函數(shù)中斷點(diǎn)調(diào)試,結(jié)果如圖labelFrame-2所示
可以看到,labelFrame在創(chuàng)建和點(diǎn)擊時(shí)不同,創(chuàng)建時(shí)是假的,點(diǎn)擊時(shí)才是真的。而 titleRect.x = btnFrame.x + labelFrame.x,因此titleRect反映的是titleLabel在按鈕的直接父視圖中的frame,而不是在按鈕中的frame。
因此在創(chuàng)建按鈕時(shí),titleLabel的大小,只能通過titleRectForContentRect:獲取。
需要注意的是,如果按鈕的寬度小于圖片和標(biāo)題寬度之和,標(biāo)題會(huì)被裁剪,有可能標(biāo)題的寬高都會(huì)被設(shè)置為0。如果要把title放到圖片下方,那么按鈕的寬度就不夠了,label會(huì)被裁剪,只能通過NSString 的 sizeWithAttributes: 方法獲取大概寬度了,或者創(chuàng)建一個(gè)臨時(shí)的足夠?qū)挾鹊陌粹o來獲取標(biāo)題寬度。(這里可以通過UILabel的 intrinsicContentSize 來獲?。?。
按鈕的imageView的大小可以直接獲取。不建議通過imageView.image.size來獲取,因?yàn)閳D片可能比按鈕大。
CGRect imageViewFrame = btn.imageView.frame;
CGRect imageRect = [btn imageRectForContentRect:btn.frame];
這兩句代碼,在創(chuàng)建按鈕和點(diǎn)擊按鈕時(shí),調(diào)試結(jié)果一樣。
和titleRect一樣,imageRect反映的是imageView在按鈕的父視圖中的frame,而不是在按鈕中的frame。
EdgeInsets
imageView和titleLabel的frame直接修改是沒有效果的,只能通過按鈕的titleEdgeInsets、imageEdgeInsets、contentEdgeInsets進(jìn)行修改。
// 屬性
@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個(gè)參數(shù),上左下右,逆時(shí)針。
// 這里左邊界增大10,如果是左對(duì)齊,就會(huì)向右平移10。如果是-10會(huì)向左平移10。
UIEdgeInsets insets = UIEdgeInsetsMake(0, 10, 0, 0);
什么是edgeInsets呢?文檔對(duì) 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:.
大意是正數(shù)會(huì)靠近c(diǎn)enter,負(fù)數(shù)會(huì)遠(yuǎn)離。
我理解為邊界的厚度,就像手機(jī)邊框。假設(shè)有個(gè)iPhone平放在桌子上,左對(duì)齊,手機(jī)到桌子的距離和手機(jī)屏幕到桌子的距離是不等的,此時(shí)手機(jī)屏幕就像按鈕的title label,屏幕到桌子的距離就像label到按鈕的距離。如果屏幕大小不變,手機(jī)到桌子的距離不變,但是又要屏幕顯示的內(nèi)容往右邊移,就只能增加手機(jī)邊框了。同理,要讓按鈕的標(biāo)題往右移動(dòng),不改變按鈕的大小和按鈕的位置,就只能通過設(shè)置edgeInsets來修改title label的邊框大小了。
邊界看不見,不影響frame的大小,但是參與布局。
Alignment
按鈕的 contentVerticalAlignment、contentHorizontalAlignment 屬性影響對(duì)齊方式,對(duì)上述三個(gè)edgeInsets的效果也有影響。
// 垂直對(duì)齊方式
typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
UIControlContentVerticalAlignmentCenter = 0,
UIControlContentVerticalAlignmentTop = 1,
UIControlContentVerticalAlignmentBottom = 2,
UIControlContentVerticalAlignmentFill = 3,
};
// 水平對(duì)齊方式
typedef NS_ENUM(NSInteger, UIControlContentHorizontalAlignment) {
UIControlContentHorizontalAlignmentCenter = 0,
UIControlContentHorizontalAlignmentLeft = 1,
UIControlContentHorizontalAlignmentRight = 2,
UIControlContentHorizontalAlignmentFill = 3,
};
// 垂直對(duì)齊方式為頂部對(duì)齊
btn.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;
// 水平對(duì)齊方式為左對(duì)齊
btn.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
// 這里左邊界增大10。如果是左對(duì)齊,就會(huì)向右平移10。如果是-10會(huì)向左平移10。
// 如果是劇中對(duì)齊,只會(huì)向右平移5。
btn.titleEdgeInsets = UIEdgeInsetsMake(0, 10, 0, 0);
Demo
按鈕默認(rèn)圖片在左,標(biāo)題在右邊。如果要交換圖片和標(biāo)題的位置,可以這樣寫:
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);
如果要標(biāo)題在圖片下方,可以這樣寫:
// 注意!按鈕比圖片小很多的時(shí)候,效果不可預(yù)測,原因不明。
- (void)createUpDownButton {
// 創(chuàng)建按鈕
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(100, 100, 82, 100);
btn.backgroundColor = [UIColor grayColor];
[btn setImage:[UIImage imageNamed:@"小小女孩"] forState:UIControlStateNormal];
[btn setTitle:@"標(biāo)題" forState:UIControlStateNormal];
[self.view addSubview:btn];
// 設(shè)置對(duì)齊方式
btn.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;
btn.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
// 獲取圖片和標(biāo)題大小
CGSize labelSize = btn.titleLabel.intrinsicContentSize;
CGSize imageSize = btn.imageView.frame.size;
// 往下平移的距離
float top = imageSize.height + 5;
// 往左平移的距離
float left = (imageSize.width + labelSize.width) / 2;
// 設(shè)置標(biāo)題偏移量,上左下右。要往左移,所以是負(fù)的。
btn.titleEdgeInsets = UIEdgeInsetsMake(top, -left, 0, 0);
// 設(shè)置內(nèi)容偏移量,上左下右,對(duì)圖片和標(biāo)題都起作用。
btn.contentEdgeInsets = UIEdgeInsetsMake(10, 10, 0, 0);
}
例子看似簡單,在不知道要通過intrinsicContentSize獲取標(biāo)題寬度、圖片大于按鈕是特殊情況、對(duì)齊方式會(huì)影響偏移量效果的時(shí)候,被坑得懷疑人生。
設(shè)置左對(duì)齊和頂部對(duì)齊實(shí)現(xiàn)起來最簡便,計(jì)算簡單,坑最少。比如按鈕寬度不夠,標(biāo)題顯示為...,可以設(shè)置為左對(duì)齊,減小左邊界往左移(理解為在左邊給它更多空間顯示)就可以正常顯示了。
計(jì)算往左的偏移量,思路是兩個(gè)控件的center.x重疊,所以偏移量就是長度之和的一半。
默認(rèn)標(biāo)題在圖片右邊,所以要往左移。減小左邊界和增大右邊界都能使標(biāo)題左移,區(qū)別是右邊界增大到一定程度就不會(huì)左移了,而是壓縮標(biāo)題成...了。左對(duì)齊的話,左邊界減小量就是平移量,居中對(duì)齊的話減小量要乘以2,或者左邊界減小的同時(shí)右邊界增大。
遇到圖片比按鈕大的,只能通過故事板慢慢調(diào)了,不清楚蘋果內(nèi)部是如何計(jì)算的。
4、總結(jié)
創(chuàng)建按鈕要調(diào)用類方法,按鈕創(chuàng)建之后不能修改類型,UIButtonTypeCustom類型才能自定義圖片。
按鈕采用 Target-Action 設(shè)計(jì)模式,action方法有三種格式,理解按鈕的各鐘事件觸發(fā)操作,順序是touch down-drag-up或者cancel。
要禁止按鈕交互,設(shè)置enabled = NO,按鈕的狀態(tài)會(huì)變成 UIControlStateDisabled,而設(shè)置 userInteractionEnabled則不會(huì)影響狀態(tài)。
按鈕狀態(tài)有5種,默認(rèn)、高亮、禁用、選中、UIControlStateFocused(好像和apple tv的按鈕相關(guān))。默認(rèn)和高亮狀態(tài)可以通過交互改變,禁用和選中狀態(tài)要通過代碼改變(enabled、selected屬性)。
按鈕的標(biāo)題和圖片,跟狀態(tài)相關(guān)的屬性要通過*setXXX: forState: *函數(shù)來設(shè)置,不能直接對(duì) titleLabel 或 imageView 進(jìn)行設(shè)置。如果圖片大于按鈕,標(biāo)題的寬高會(huì)變?yōu)?。
在創(chuàng)建按鈕時(shí),通過UILabel的intrinsicContentSize獲取標(biāo)題的原始寬度。訪問過按鈕的titleLabel后,可以通過UIButton的titleRectForContentRect:獲取標(biāo)題在按鈕的直接父視圖中的frame,而不是在按鈕中的frame。按鈕的imageView的大小就是imageView.frame.size。
按鈕的標(biāo)題和圖片的大小和位置只能通過titleEdgeInsets、imageEdgeInsets、contentEdgeInsets進(jìn)行修改。正數(shù)靠近中心(壓縮空間),負(fù)數(shù)遠(yuǎn)離中心(擴(kuò)展空間)。
按鈕的 contentVerticalAlignment、contentHorizontalAlignment 屬性影響對(duì)齊方式,對(duì)上述三個(gè)edgeInsets的效果也有影響。