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的效果也有影响。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容

  • 一个UIButton的实例变量, 使一个按钮(button)在触摸屏上生效。一个按钮监听触摸事件,当被点击时,给目...
    wushuputi阅读 1,470评论 0 1
  • 各位童鞋们, UIButton是iOS中常用的控件,下面来详细介绍它的使用方法和以及开发中需要注意的问题. UIB...
    我与太阳肩并肩阅读 1,258评论 2 17
  • 对象继承关系 UIButton 类本身定义继承 UIControl ,描述了在 iOS 上所有用户界面控件的常见基...
    独木舟的木阅读 3,709评论 0 3
  • 前言:UI控件整理之UIButton 一、显示图片(复选框) UIButton *button = [UIButt...
    心如止水的鱼阅读 261评论 0 0
  • 店面规模扩大了,需要人员补充。我负责招聘新人。 在店门口张贴了招聘启示。不是找工作的时机,前来咨询的...
    红叶舞秋兮阅读 187评论 0 0