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有关,具体不太清楚。
按钮状态.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会被裁剪,只能通过NSString 的 sizeWithAttributes: 方法获取大概宽度了,或者创建一个临时的足够宽度的按钮来获取标题宽度。(这里可以通过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的效果也有影响。
网友评论