UIButton

作者: 阿斯兰iOS | 来源:发表于2017-04-24 13:50 被阅读98次

    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

          本文链接:https://www.haomeiwen.com/subject/uyfbbttx.html