UIButton

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。

按鈕事件.png

黃色的區域是多大呢?官方文檔說的是 *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有關,具體不太清楚。

按鈕狀態.png

設置按鈕的 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,


uibutton_callouts.png

按鈕可以同時顯示一張圖片、標題和背景圖片。默認圖片在左邊,文字在右邊。視圖層次從上往下是標題、圖片、背景圖片。

注意了,如果設置的圖片大小超出按鈕的寬度,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];

按鈕內容.png

注意,按鈕的 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-1.png

在點擊按鈕的處理函數中斷點調試,結果如圖labelFrame-2所示

labelFrame-2.png

可以看到,labelFrame在創建和點擊時不同,創建時是假的,點擊時才是真的。而 titleRect.x = btnFrame.x + labelFrame.x,因此titleRect反映的是titleLabel在按鈕的直接父視圖中的frame,而不是在按鈕中的frame。

因此在創建按鈕時,titleLabel的大小,只能通過titleRectForContentRect:獲取。

需要注意的是,如果按鈕的寬度小于圖片和標題寬度之和,標題會被裁剪,有可能標題的寬高都會被設置為0。如果要把title放到圖片下方,那么按鈕的寬度就不夠了,label會被裁剪,只能通過NSStringsizeWithAttributes: 方法獲取大概寬度了,或者創建一個臨時的足夠寬度的按鈕來獲取標題寬度。(這里可以通過UILabel的 intrinsicContentSize 來獲取)。

按鈕的imageView的大小可以直接獲取。不建議通過imageView.image.size來獲取,因為圖片可能比按鈕大。

CGRect imageViewFrame = btn.imageView.frame;
CGRect imageRect = [btn imageRectForContentRect:btn.frame];

這兩句代碼,在創建按鈕和點擊按鈕時,調試結果一樣。

imageViewFrame-1.png
imageViewFrame-2.png

和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);

標題在左.png

如果要標題在圖片下方,可以這樣寫:

// 注意!按鈕比圖片小很多的時候,效果不可預測,原因不明。
- (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);
}
標題在下.png

例子看似簡單,在不知道要通過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的效果也有影響。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一個UIButton的實例變量, 使一個按鈕(button)在觸摸屏上生效。一個按鈕監聽觸摸事件,當被點擊時,給目...
    wushuputi閱讀 1,544評論 0 1
  • 各位童鞋們, UIButton是iOS中常用的控件,下面來詳細介紹它的使用方法和以及開發中需要注意的問題. UIB...
    我與太陽肩并肩閱讀 1,334評論 2 17
  • 對象繼承關系 UIButton 類本身定義繼承 UIControl ,描述了在 iOS 上所有用戶界面控件的常見基...
    獨木舟的木閱讀 3,766評論 0 3
  • 前言:UI控件整理之UIButton 一、顯示圖片(復選框) UIButton *button = [UIButt...
    心如止水的魚閱讀 294評論 0 0
  • 店面規模擴大了,需要人員補充。我負責招聘新人。 在店門口張貼了招聘啟示。不是找工作的時機,前來咨詢的...
    紅葉舞秋兮閱讀 201評論 0 0