首先,说一下需要考虑这个问题的场景
- 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的整体位置,包括文字背景所占位置,都可以灵活设置。
# 写法三:利用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,即上图中白色框与蓝色框都与最外层边缘线重合。
设置了UIEdgeInsets时,即给了上图(内边距示意图)中top, left, bottom, right的红线段一个长度,概括的来说,值为正的时候,对应红色线条向里伸长,为负时,向外伸长。毕竟叫做“内边距”~
# 更准确的来说,top和bottom为一对,用来控制纵向可变区域(黑色线框);left和right为一对,用来控制横向可变区域(白色线框)。
-
测试:
经过很多尝试,得出以下结论伪代码
:
-
UIButton设置了图片和标题时,默认图片在左标题在右。且图片紧贴文字,整体内容上下左右居中显示,并重置了titleLabel和imageView的UIEdgeInsets均为 (0,0,0,0),打印可得。
- 图片文字均居中:在默认左图右字的基础上,计算一下,图片需要右移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)];
- 左文字右图片:在默认的基础上,计算一下,图片需要右移titleWidth,文字 需要左移imageWidth,从而达到图片文字调换位置的效果(这种情况比较好理解,也可以借助用来理解上一个)
[btn setImageEdgeInsets:UIEdgeInsetsMake(0, TitleWidth, 0, -TitleWidth)]; [btn setTitleEdgeInsets:UIEdgeInsetsMake(0, -imageWidth, 0, imageWidth)];
- 上图片下文字:在默认的基础上,图片需要先右移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)];
- 上文字下图片:在默认的基础上,图片需要先右移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方法,验证是在前一次的基础上继续偏移,还是以后一次的设置为准。亲测为后者。
-
在以上实现的过程中,可能会遇到按钮图片大小不合适,需要调整(一般是需要缩小)
思路大致有两个:
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)];
以上。