引言
在实际项目开发中,按钮是一个非常普遍的UI组件。除了纯文本或纯图片之外,图片与文本组合的按钮也非常常见。尽管按钮支持同时设置图片和文本,但样式较为单一。在iOS 15之前,默认样式只支持图片在前、文本紧随其后,但UI设计往往要求更多样化的布局,如文本在前、图片在后,或者文本在下、图片在上。此外,为了增强页面的美观效果,通常还需要在图片与文本之间,或文本与按钮之间添加一些间距。
面对这些需求,我们需要对按钮的布局进行手动调整。在这篇博客中,我将分享几种常见的布局调整方法,帮助你在开发中轻松实现更丰富的按钮样式。
方案介绍
其实调整布局的方案有很多:
自定义按钮
比如我们可以继承自UIControl来自定义按钮,不过这意味着我们抛弃了系统为我们提供的UIButton,一切内容都需要我们自己来实现,代码量会比较竟然,并且它的性能和健壮性可能也不如系统为我们提供的按钮更好。
重写布局
也可以通过重写imageRect(forContentRect contentRect: CGRect) -> CGRect方法和titleRect(forContentRect contentRect: CGRect) -> CGRect 方法来直接设置按钮中图片和文字的frame。但是它通常不能很好的适配自动布局,而在我们的实际开发中自动布局占绝大部分,如果不能适配自动布局恐怕不太理想。
调整EdgeInsets
通过调整图片和文字的EdgeInsets,这个方案很好既保留了系统的UIButton又可以完美的适应自动布局。
下面我就来具体讨论使用调整titleEdgeInsets和imageEdgeInsets实现调整按钮布局的方案。
方案实现
简单案例
回想一下在开发中,我们使用自动布局创建一个图片+文字的按钮通常情况下会这样做:
let button = UIButton()
button.setTitle("Home", for: .normal)
button.setTitleColor(.black, for: .normal)
button.setImage(UIImage(named: "tabbar_feed_icon_selected"), for: .normal)
button.backgroundColor = .red
self.view.addSubview(button)
button.snp.makeConstraints { make in
make.center.equalToSuperview()
}
为了方便查看按钮的大小,为按钮添加了红色的背景,运行效果如下:
当我们想要调整图片和按钮的间距时,比如添加8的间距,通常会这样做:
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 4)
button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: -4)
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
运行效果如下:
那么使用相同的方案,我们只需要通过计算就调整图片和文案在按钮中的相对位置。
完整实现
1.创建PHButton
1.继承自UIButton创建一个PHButton,并为其添加一些基本属性,图片按钮排列类型,图片按钮间距,以及内容距离按钮左右和上下的间距。
class PHButton: UIButton {
/// 按钮类型
var type:PHButtonType = .imageLeft {
didSet {
if #available(iOS 15.0, *) {
config15()
}
}
}
/// 图文间距
var space:CGFloat = 0.0 {
didSet {
if #available(iOS 15.0, *) {
config15()
}
}
}
/// 横向padding
var horPadding:CGFloat = 0.0 {
didSet {
if #available(iOS 15.0, *) {
config15()
}
}
}
/// 纵向padding
var verPadding:CGFloat = 0.0 {
didSet {
if #available(iOS 15.0, *) {
config15()
}
}
}
....
}
其中config15是iOS15之后按钮样式的调整方案,后面会介绍它,但即使我们不调用它,也完全OK。
添加自定义的初始化方法:
init(type: PHButtonType = .imageLeft, space: CGFloat = 0.0, horPadding: CGFloat = 0.0, verPadding: CGFloat = 0.0) {
super.init(frame: .zero)
self.type = type
self.space = space
self.horPadding = horPadding
self.verPadding = verPadding
}
2.枚举
定义常见按钮布局类型的枚举:
enum PHButtonType {
case imageLeft
case imageRight
case imageTop
case imageBottom
}
3.计算样式布局
布局imageLeft样式:
/// 图片在左边
private func configImageLeft() {
let half = space / 2
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: half, bottom: 0, right: -half)
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: -half, bottom: 0, right: half)
self.contentEdgeInsets = UIEdgeInsets(top: verPadding, left: half + horPadding, bottom: verPadding, right: half + horPadding)
}
布局imageRight样式:
/// 图片在右边
private func configImageRight() {
let half = space / 2
// 图片宽度
let imageWidth = imageView?.frame.size.width ?? 0
// 文字宽度
let titleWidth = titleLabel?.frame.size.width ?? 0
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth - half, bottom: 0, right: imageWidth + half)
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: titleWidth + half, bottom: 0, right: -titleWidth - half)
self.contentEdgeInsets = UIEdgeInsets(top: verPadding, left: half + horPadding, bottom: verPadding, right: half + horPadding)
}
布局imageTop样式:
/// 图片在上边
private func configImageTop() {
let half = space / 2
// 图片高度
let imageHeight = imageView?.frame.size.height ?? 0
// 图片宽度
let imageWidth = imageView?.frame.size.width ?? 0
// 文字高度
let titleHeight = titleLabel?.frame.size.height ?? 0
// 文字宽度
let titleWidth = titleLabel?.frame.size.width ?? 0
self.titleEdgeInsets = UIEdgeInsets(top: imageHeight + half, left: -imageWidth, bottom: 0, right: 0)
self.imageEdgeInsets = UIEdgeInsets(top: -titleHeight - half, left: 0, bottom: 0, right: -titleWidth)
// 取出文字的宽度和图片的宽度的最大值
let maxWidth = max(imageWidth, titleWidth)
// 取出文字的高度和图片的高度的和
let maxHeight = imageHeight + titleHeight
// 内容原始宽度
let contentWidth = imageWidth + titleWidth
// 内容原始高度
let contentHeight = max(imageHeight, titleHeight)
// 计算内边距
let contentInsetWidth = (maxWidth - contentWidth) / 2
let contentInsetHeight = (maxHeight - contentHeight) / 2
self.contentEdgeInsets = UIEdgeInsets(top: verPadding + contentInsetHeight, left: horPadding + contentInsetWidth, bottom: verPadding + contentInsetHeight, right: horPadding + contentInsetWidth)
}
布局imageBottom样式:
/// 图片在下边
private func configImageBottom() {
let half = space / 2
// 图片高度
let imageHeight = imageView?.frame.size.height ?? 0
// 图片宽度
let imageWidth = imageView?.frame.size.width ?? 0
// 文字高度
let titleHeight = titleLabel?.frame.size.height ?? 0
// 文字宽度
let titleWidth = titleLabel?.frame.size.width ?? 0
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth, bottom: imageHeight + half, right: 0)
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -titleHeight - half, right: -titleWidth)
// 取出文字的宽度和图片的宽度的最大值
let maxWidth = max(imageWidth, titleWidth)
// 取出文字的高度和图片的高度的和
let maxHeight = imageHeight + titleHeight
// 内容原始宽度
let contentWidth = imageWidth + titleWidth
// 内容原始高度
let contentHeight = max(imageHeight, titleHeight)
// 计算内边距
let contentInsetWidth = (maxWidth - contentWidth) / 2
let contentInsetHeight = (maxHeight - contentHeight) / 2
self.contentEdgeInsets = UIEdgeInsets(top: verPadding + contentInsetHeight, left: horPadding + contentInsetWidth, bottom: verPadding + contentInsetHeight, right: horPadding + contentInsetWidth)
}
其中图片在上和图片在下的当我们计算时需要考虑的因素需要多一些,代码中的注释也很清晰了。
最后记得容器的宽高,需要是图片+文字+间距+间距的宽高。
4.调整布局
重写layoutSubviews方法,根据枚举类型调用布局调整方法:
override func layoutSubviews() {
super.layoutSubviews()
//ios 15
if #available(iOS 15.0, *) {
return
}
if let _ = imageView, let _ = titleLabel {
switch type {
case .imageLeft:
configImageLeft()
break
case .imageRight:
configImageRight()
break
case .imageTop:
configImageTop()
break
case .imageBottom:
configImageBottom()
break
}
}
}
5.iOS15
不过呢iOS15 之后苹果为UIButton引入了一个新的属性configuration,这个新属性允许开发者通过UIButton.Configuration
来设置按钮的外观和布局,而不需要再通过titleEdgeInsets
、imageEdgeInsets
和contentEdgeInsets
这些属性来调整按钮中文字和图片的布局。
这就意味着我们可以在创建的同时就设置了这些内容,不过我们继承自UIButton的PHButton也可以使用自己的方法来使用新的适配方式:
private func config15() {
var imagePlacement: NSDirectionalRectEdge = .leading
switch type {
case .imageLeft:
imagePlacement = .leading
case .imageRight:
imagePlacement = .trailing
case .imageTop:
imagePlacement = .top
case .imageBottom:
imagePlacement = .bottom
}
var configuration = UIButton.Configuration.plain()
configuration.imagePlacement = imagePlacement
configuration.imagePadding = space
configuration.contentInsets = NSDirectionalEdgeInsets(top: verPadding, leading: horPadding, bottom: verPadding, trailing: horPadding)
self.configuration = configuration
}
并且代码也简单多了,也不需要任何计算了。
效果展示
下面我们来使用PHButton创建不同布局的按钮,我们就来创建4个按钮吧:
let types = [PHButtonType.imageLeft, PHButtonType.imageTop, PHButtonType.imageRight, PHButtonType.imageBottom]
var offsetX = 50.0
var offsetY = 200.0
for (index, type) in types.enumerated() {
let button = PHButton()
button.setTitle("Home", for: .normal)
button.setTitleColor(.black, for: .normal)
button.setImage(UIImage(named: "tabbar_feed_icon_selected"), for: .normal)
button.backgroundColor = .red
button.type = type
button.space = 10.0
button.horPadding = 5.0
button.verPadding = 5.0
self.view.addSubview(button)
if index % 2 == 0 {
offsetX = 50.0
offsetY = offsetY + 80.0
}
button.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(offsetX)
make.top.equalToSuperview().offset(offsetY)
}
offsetX = offsetX + 120.0
}
效果如下:
结语
通过本文的介绍,我们深入探讨了在iOS开发中调整按钮图片和文本布局的多种方式。无论是在iOS 15之前还是之后,你都可以根据项目需求灵活应用这些方案,打造出更符合设计预期的按钮样式。希望这些技巧和示例能够帮助你在日常开发中更加自如地处理按钮的布局需求,为用户提供更佳的视觉和交互体验。