UIButton自定义图片文字相对位置?layoutSubviews?UIEdgeInsetsMake?

首先,说一下需要考虑这个问题的场景

  • UIButton ,同时设置图片和文字,默认图片在左,文字在右
  • 需求经常可能是图片上文字下?文字左图片右?文字上图片下?可能位置还需要动态变化或动态展示隐藏

查了很久,大致有以下三种做法:

  • 1、简单粗暴-直接写个UIImageView,用相对布局盖上去
    好处是动态变动的,针对动态变化的需求操作起来比较灵活。但总觉得这么写不大专业?
  • 2、重写UIButton的layoutSubview方法
    好处是比较直观,符合正常布局设计思维
  • 3、利用UIButton里的setTitleEdgeInsets和setImageInsets方法,调整文字和图片的位置及大小
    好处是代码量少,但是比较不好理解

下面详细写下后两种写法:

# 写法二:重写UIButton的layoutSubview方法

layoutSubviews是对subviews重新布局。比如,我们想更新子视图的位置的时候,可以通过调用layoutSubviews方法,即可以实现对子视图重新布局。
layoutSubviews默认是不做任何事情的,用到的时候,需要在子类进行重写。

layoutSubviews调用场景
①、直接调用setLayoutSubviews。
②、addSubview的时候触发layoutSubviews。
③、当view的frame发生改变的时候触发layoutSubviews。
④、第一次滑动UIScrollView的时候触发layoutSubviews。
⑤、旋转Screen会触发父UIView上的layoutSubviews事件。
⑥、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
注意:
init初始化不会触发layoutSubviews,但是使用initWithFrame进行初始化时,当rect的值不为CGRectZero时,也会触发。
引用自:iOS-layoutSubviews

结合工厂设计模式,再考虑选择分类还是继承的写法?由于所写类需要一个type属性,来标志按钮图片及标题属于哪一种位置关系;而分类一般不能添加属性(可能会覆盖原有类的属性),同时分类中的方法都添加给了原有类,可能会影响原有类及其子类的使用。所以这里采用继承的写法比较合适 LQButton:UIButton

  • 具体实现代码如下:
//  LQButton.h
#import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, LQButtonType){
    LQButtonTypeCenterImageCenterTitle,//图和文字都居中
    LQButtonTypeLeftImageRightTitle,//左图右文字
    LQButtonTypeLeftTitleRightImage,//左文字右图
    LQButtonTypeTopImageBottomTitle,//上图下文字 略
    LQButtonTypeTopTitleBottomImage//上文字下图 略
};

@interface LQButton : UIButton

@property (nonatomic, assign) LQButtonType type;

//初始化一个按钮的同时设置其图片文字位置关系
+ (instancetype)lqButtonWithType:(LQButtonType)buttonType;
//更改按钮的图片文字位置关系
- (void)updateButtonStyleWithType:(LQButtonType)buttonType;

@end
//  LQButton.m
#import "LQButton.h"

@implementation LQButton

+ (instancetype)lqButtonWithType:(LQButtonType)buttonType {
    LQButton *btn = [LQButton buttonWithType:UIButtonTypeCustom];
    btn.type = buttonType;
    return btn;
}

- (void)updateButtonStyleWithType:(LQButtonType)buttonType {
    self.type = buttonType;
    [self layoutSubviews];
}

- (void)layoutSubviews {
    [super layoutSubviews];
    if (self.type == LQButtonTypeCenterImageCenterTitle) {
        
        [self resetBtnCenterImageCenterTitle];
        
    }else if (self.type == LQButtonTypeLeftImageRightTitle) {
        
        [self resetBtnLeftImageRightTitle];
        
    }else if (self.type == LQButtonTypeLeftTitleRightImage) {
        
        [self resetBtnLeftTitleRightImage];
        
    }
}

- (void)resetBtnCenterImageCenterTitle {
    self.imageView.frame = self.bounds;
    [self.imageView setContentMode:UIViewContentModeCenter];
    
    self.titleLabel.frame = self.bounds;
    self.titleLabel.textAlignment = NSTextAlignmentCenter;
}

- (void)resetBtnLeftImageRightTitle {
    
    CGRect frame = self.bounds;
    frame.size.width *= 0.5;
    self.imageView.frame = frame;
    [self.imageView setContentMode:UIViewContentModeCenter];
    
    frame.origin.x = (self.bounds.size.width - frame.size.width);
    self.titleLabel.frame = frame;
    self.titleLabel.textAlignment = NSTextAlignmentCenter;
}

- (void)resetBtnLeftTitleRightImage {
    
    CGRect frame = self.bounds;
    frame.size.width *= 0.5;
    self.titleLabel.frame = frame;
    self.titleLabel.textAlignment = NSTextAlignmentCenter;
    
    frame.origin.x = (self.bounds.size.width - frame.size.width);
    self.imageView.frame = frame;
    [self.imageView setContentMode:UIViewContentModeCenter];
}

@end

以上是自定义LQButton的实现,具体引用如下:

//  ViewController.m
// 先引入头文件 #import "LQButton.h"
// 然后开始使用
- (void)viewDidLoad {
    [super viewDidLoad];
    UIImage *btnImg = [UIImage imageNamed:@"btnImg"];
//这种通过缩放图片达到设置图片大小的方法,会导致图片清晰度下降,文章最后有进行分析
    btnImg = [self imageWihtoutScale:btnImg size:CGSizeMake(50, 50)];

    LQButton *btn3 = [LQButton lqButtonWithType:LQButtonTypeLeftTitleRightImage];
    btn3.frame = CGRectMake(80, 590, 300, 80);
    btn3.backgroundColor = [UIColor lightGrayColor];
    [btn3 setImage:btnImg forState:UIControlStateNormal];
    [btn3 setTitle:@"左文字右图片" forState:UIControlStateNormal];
    btn3.titleLabel.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.8];
    [self.view addSubview:btn3];
    
    LQButton *btn4 = [LQButton lqButtonWithType:LQButtonTypeLeftImageRightTitle];
    btn4.frame = CGRectMake(80, 680, 300, 80);
    btn4.backgroundColor = [UIColor lightGrayColor];
    [btn4 setImage:btnImg forState:UIControlStateNormal];
    [btn4 setTitle:@"左图片右文字" forState:UIControlStateNormal];
    btn4.titleLabel.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.8];
    [self.view addSubview:btn4];
    
    LQButton *btn5 = [LQButton lqButtonWithType:LQButtonTypeCenterImageCenterTitle];
    btn5.frame = CGRectMake(80, 770, 300, 80);
    btn5.backgroundColor = [UIColor lightGrayColor];
    [btn5 setImage:btnImg forState:UIControlStateNormal];
    [btn5 setTitle:@"图片文字均居中" forState:UIControlStateNormal];
    btn5.titleLabel.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.8];
//注意:用重写layoutSubviews的方式,frame改变、及addSubviews的时候,均会触发layoutSubviews。
//所以如果在此处再使用setTitleEdgeInsets/setImageEdgeInsets,将不会有效果。
//只能通过LQButton中的updateButtonStyleWithType方法来更新文字图片的相对位置。
    [self.view addSubview:btn5];
}

效果图如下:
可以看出,通过frame设置位置,titleLabel的整体位置,包括文字背景所占位置,都可以灵活设置。


效果呈现·titleLabel的背景色设置了透明度

层级图

# 写法三:利用UIButton里的setTitleEdgeInsets和setImageInsets方法,调整文字和图片的位置及大小

其中最重要的是理解UIEdgeInsetsMake做了什么事?
先贴网上流传最广的一张图:引用自一叶博客

控件内边距示意图

// UIEdgeInsets定义
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;

UIEdgeInsets是一个结构体,分别对控件top, left, bottom, right四个内边距进行设置。

图中,蓝色标识为可变区域, 绿色标识为不变区域。UIEdgeInsets结构体的属性top与bottom为一对,用来指定纵向可变区域(黑色虚线矩形),left与right为一对,用来指定横向可变区域(白色虚线矩形)。当UIButton/UIImageView的size大于UIImage的size时(假设只有图片),会调整图片中可变区域大小以铺满整个控件,具体调整规则如下:
(1)控件宽度大于图片宽度,拉伸白色虚线矩形
# 即白色虚线矩形变宽,拉宽图片,以铺满整个控件
(2)控件高度大于图片高度,拉伸黑色虚线矩形
# 即黑色虚线矩形变高,拉长图片,以铺满整个控件
(3)控制宽度小于图片宽度时,横向整体缩小(可变区与不变区比例不变)
(4)控制高度小于图片高度时,纵向整体缩小(可变区与不变区比例不变)
# 与UIViewContentMode有关,默认为UIViewContentModeScaleToFill:图片拉伸填充至整个UIImageView(图片可能会变形)
(标 # 的为个人理解)

【重点·理解】

  • 因为单个空间的UIEdgeInsets,不设置时,默认内边距都是0,即上图中白色框与蓝色框都与最外层边缘线重合。


    初始0内边距
  • 设置了UIEdgeInsets时,即给了上图(内边距示意图)中top, left, bottom, right的红线段一个长度,概括的来说,值为正的时候,对应红色线条向里伸长,为负时,向外伸长。毕竟叫做“内边距”~

  • # 更准确的来说,top和bottom为一对,用来控制纵向可变区域(黑色线框);left和right为一对,用来控制横向可变区域(白色线框)。

  • 测试:
    经过很多尝试,得出以下结论 伪代码

    默认位置关系

    1. UIButton设置了图片和标题时,默认图片在左标题在右。且图片紧贴文字,整体内容上下左右居中显示,并重置了titleLabel和imageView的UIEdgeInsets均为 (0,0,0,0),打印可得。


      图片文字均居中
    2. 图片文字均居中:在默认左图右字的基础上,计算一下,图片需要右移TitleWidth/2,即:
    [btn setImageEdgeInsets:UIEdgeInsetsMake(0, TitleWidth/2, 0, -TitleWidth/2)];
    

    另一种思路,是右边距直接-titleWidth,向右扩大一个标题的宽度,而图片会自己居中显示,所以可以达到一样的居中效果

    [btn setImageEdgeInsets:UIEdgeInsetsMake(0, 0, 0, -TitleWidth)];
    

    文字同理,需要左移imageWidth/2,或直接左边距扩大一个图片的宽度。

    [btn setTitleEdgeInsets:UIEdgeInsetsMake(0, -imageWidth/2, 0, imageWidth/2)];
    [btn setTitleEdgeInsets:UIEdgeInsetsMake(0, -imageWidth, 0, 0)];
    
    左文字右图片
    1. 左文字右图片:在默认的基础上,计算一下,图片需要右移titleWidth,文字 需要左移imageWidth,从而达到图片文字调换位置的效果(这种情况比较好理解,也可以借助用来理解上一个)
    [btn setImageEdgeInsets:UIEdgeInsetsMake(0, TitleWidth, 0, -TitleWidth)];
    [btn setTitleEdgeInsets:UIEdgeInsetsMake(0, -imageWidth, 0, imageWidth)];
    
    上图片下文字
    1. 上图片下文字:在默认的基础上,图片需要先右移titleWidth/2,再上移titleHeight/2,文字先左移imageWidth/2,再下移imageHeight/2。即:
    [btn setImageEdgeInsets:UIEdgeInsetsMake(-titleHeight/2, TitleWidth/2, titleHeight/2, -TitleWidth/2)];
    [btn setTitleEdgeInsets:UIEdgeInsetsMake(imageHeight/2, -imageWidth/2, -imageHeight/2, imageWidth/2)];
    

    当然,也可以用第二种思路:

    [btn setImageEdgeInsets:UIEdgeInsetsMake(0, TitleWidth, titleHeight, 0)];
    [btn setTitleEdgeInsets:UIEdgeInsetsMake(imageHeight, 0, 0, imageWidth)];
    
    上文字下图片
    1. 上文字下图片:在默认的基础上,图片需要先右移titleWidth/2,再下移titleHeight/2,文字先左移imageWidth/2,再上移imageHeight/2。
    [btn setImageEdgeInsets:UIEdgeInsetsMake(titleHeight/2, TitleWidth/2, -titleHeight/2, -TitleWidth/2)];
    [btn setTitleEdgeInsets:UIEdgeInsetsMake(-imageHeight/2, -imageWidth/2, imageHeight/2, imageWidth/2)];
    

    第二种思路的就省略了....

【注意!】以上所取的width, height,要使用button.titleLabel.intrinsicContentSize.width计算titleLabel的宽度(tips:使用button.titleLabel.bounds.size.width的在iOS8以上会得到宽度为0的结果,造成错误的结果)

网上说的最多的,理解为偏移量,不太准确。可以认为是在初始0边距的基础上,进行偏移。最好的验证方法,就是写两次setTitleEdgeInsets/setImageInsets方法,验证是在前一次的基础上继续偏移,还是以后一次的设置为准。亲测为后者。

  1. 在以上实现的过程中,可能会遇到按钮图片大小不合适,需要调整(一般是需要缩小)
    思路大致有两个:
    a. 图片缩放后再设置为按钮的image,但是这样操作后图片会变模糊!

    UIImage *btnImg = [UIImage imageNamed:@"btnImg"];
    btnImg = [self imageWihtoutScale:btnImg size:CGSizeMake(50, 50)];
    [btn setImage:btnImg forState:UIControlStateNormal];
    

    b. 还是利用UIEdgeInsetsMake,再加上setContentMode UIViewContentMode详解
    可以通过同时增加top-bottom/left-right,来缩小内边距。
    同时,之前提过,图片默认ContentMode 为UIViewContentModeScaleToFill:图片拉伸填充至整个UIImageView(图片可能会变形),所以在调整内边距前,修改[btn.imageView setContentMode:UIViewContentModeScaleAspectFit];
    UIViewContentModeScaleAspectFill
    //图片拉伸至图片的的宽度或者高度等于UIImageView的宽度或者高度为止,看图片的宽高哪一边最接近UIImageView的宽高,一个属性相等后另一个就停止拉伸。这样就可以保证图片不会变形
    具体写一个:

    左文字右图片

    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(80, 300, 300, 180);
    btn1.backgroundColor = [UIColor lightGrayColor];
    [btn1 setTitle:@"按钮" forState:UIControlStateNormal];
    btn1.titleLabel.backgroundColor = [UIColor grayColor];
    UIImage *btnImg1 = [UIImage imageNamed:@"Image1"];
    [btn1 setImage:btnImg1 forState:UIControlStateNormal];
    
    CGFloat titleWidth = btn1.titleLabel.intrinsicContentSize.width;
    CGFloat imgWidth = btn1.imageView.intrinsicContentSize.width;
    
    [btn1 setTitleEdgeInsets:UIEdgeInsetsMake(0, -imgWidth, 0, imgWidth)];
    [btn1 setImageEdgeInsets:UIEdgeInsetsMake(0, titleWidth, 0, -titleWidth)];
    [self.view addSubview:btn1];
    

    只要改两行代码就可以缩小图片,且图片不会模糊。(注意,如果使用了a思路中的图片缩放,再改变图片内边距将不起作用)


    缩小图片
    [btn1.imageView setContentMode:UIViewContentModeScaleAspectFit];
    [btn1 setImageEdgeInsets:UIEdgeInsetsMake(50, titleWidth, 50, -titleWidth)];
    

以上。

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

推荐阅读更多精彩内容