按鈕需要同時(shí)設(shè)置圖片和文案且兩者的相對(duì)位置不一定,這個(gè)需求十分常見,稍有經(jīng)驗(yàn)的開發(fā)者一定會(huì)想到自己寫個(gè)可以自適應(yīng)各種情況的擴(kuò)展方法,以前需要用的時(shí)候找過一些別人寫的,拷貝過來發(fā)現(xiàn)并不能適應(yīng)所有情況,后來自己研究下寫了一個(gè),可適配項(xiàng)目中各種使用場(chǎng)景。最近工作比較空一些想把以前積累的一些東西重新回顧總結(jié)一下,就先把這個(gè)分享出來。
首先要了解UIButton內(nèi)部是怎么布局的
- UIButton同時(shí)設(shè)置了image和title的情況下默認(rèn)布局是圖片在左、文字在右,圖片和文字之間間距為0,圖片和文字整體居中顯示
- UIButton提供了imageEdgeInsets、titleEdgeInsets這兩個(gè)屬性,便是用于調(diào)整兩個(gè)控件的相對(duì)位置,imageEdgeInsets中的top、left、bottom相對(duì)于UIButton,right相對(duì)于title的left,同理,titleEdgeInsets中的top、bottom、right相對(duì)于UIButton,left相對(duì)于image的right,每個(gè)方向的賦值:正值代表這個(gè)方向的內(nèi)邊距增大,負(fù)值代表減小。
- 如果要圖片相比原始位置向左偏移5,那么可以設(shè)置
self.imageEdgeInsets = UIEdgeInsets.init(top: 0, left: -5, bottom: 0, right: 5)
左右兩邊都要設(shè)置,如果只設(shè)置left:-5或者只設(shè)置right:5的話,偏移量只有2.5
- 如果想要圖片相比原始位置往左偏移5,往下偏移10,則設(shè)置
self.imageEdgeInsets = UIEdgeInsets.init(top: 10, left: -5, bottom: -10, right: 5)
根據(jù)以上的了解我們可以開始處理以下4種情況:

1. 圖片在左,文案在右,間距20
默認(rèn)即是圖片在左,所以只考慮間距就可以了
let spacing = 20
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: -spacing/2, bottom: 0, right: spacing/2)
self..titleEdgeInsets = UIEdgeInsets(top: 0, left: spacing/2, bottom: 0, right: -spacing/2)

2. 圖片在右,文案在左,間距20
這里要考慮到圖片和文案本身的寬高
let imageSize = self.imageView?.image?.size ?? .zero
let titleSize = self.titleRect(forContentRect: self.frame).size//系統(tǒng)為titleLabel分配的size
*這里注意下,獲取系統(tǒng)為文案提供的size,不一定是文案完全展示所需的size,原因后面會(huì)解釋;圖片和文案水平布局的情況下不需要考慮這個(gè)問題*
let spacing = 20
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: titleSize.width + spacing/2, bottom: 0,
right: -(titleSize.width + spacing/2))
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: -(imageSize.width + spacing/2), bottom: 0, right: imageSize.width + spacing/2)
3. 圖片在上,文案在下,間距spacing
let imageTop = -(titleSize.height/2 + spacing/2)
let titleTop = imageSize.height/2 + spacing/2
self.imageEdgeInsets = UIEdgeInsets(top: imageTop, left: titleSize.width/2, bottom: -imageTop, right: -titleSize.width/2)
self.titleEdgeInsets = UIEdgeInsets(top: titleTop, left: -imageSize.width/2, bottom: -titleTop, right: imageSize.width/2)
4. 圖片在下,文案在上,間距spacing
let imageTop = titleSize.height/2 + spacing/2
let titleTop = -(imageSize.height/2 + spacing/2)
self.imageEdgeInsets = UIEdgeInsets(top: imageTop, left: titleSize.width/2, bottom: -imageTop, right: -titleSize.width/2)
self.titleEdgeInsets = UIEdgeInsets(top: titleTop, left: -imageSize.width/2, bottom: -titleTop, right: imageSize.width/2)
特殊情況
還記得我開頭說的之前拷貝過別人寫的不能很好地適配各種情況嗎?
當(dāng)圖標(biāo)和文字上下布局但圖片和文案所需寬度之和大于button的實(shí)際寬度時(shí),默認(rèn)先保證圖片的完整顯示同時(shí)將文字部分縮小,導(dǎo)致系統(tǒng)給分配的寬度不足以完整顯示文字,這個(gè)時(shí)候上面的布局方法就不能完美自適應(yīng)了可能會(huì)出現(xiàn)水平方向上位置異常的現(xiàn)象,這種情況需要特殊處理一下,還記得前面說的注意點(diǎn)嗎,為什么獲取系統(tǒng)分配的size,主要是用在這里判斷文案size是否被系統(tǒng)擠壓了
var titleNeedSize: CGSize = .zero//展示文字實(shí)際所需的size
if let font = self.titleLabel?.font {
titleNeedSize = title.size(withAttributes: [NSAttributedString.Key.font: font])
}
var isTitleCompress = false//文字是否被系統(tǒng)壓縮
if isSureTitleCompress {
isTitleCompress = true
} else if titleNeedSize.width > titleSize.width {
isTitleCompress = true
}
isSureTitleCompress這個(gè)變量是該設(shè)置方法的一個(gè)參數(shù),setImage(image anImage: UIImage?, title: String, imagePosition: ImagePosition, additionalSpacing: CGFloat, isSureTitleCompress: Bool = false)因?yàn)檫壿嬛惺峭ㄟ^獲取系統(tǒng)分配文案size和實(shí)際所需size比較,決定走哪個(gè)分支邏輯的,如果明確知道button設(shè)置的尺寸會(huì)導(dǎo)致文案被擠壓且可能在button的生命周期多次調(diào)用這個(gè)方法的話需要把這個(gè)參數(shù)置為true,否則會(huì)有問題;試想一下,如果沒有設(shè)置為true,第一次調(diào)用這個(gè)方法時(shí)邏輯沒問題,第二次再調(diào)用時(shí)獲取的titleSize已經(jīng)滿足了實(shí)際展示的需求,就會(huì)走到文案沒被擠壓的邏輯分支中
圖片在上,文案在下的情況最終處理應(yīng)該是這樣
let imageTop = -(titleSize.height/2 + spacing/2)
let titleTop = imageSize.height/2 + spacing/2
if isTitleCompress {
let imageLeft = (self.bounds.size.width - imageSize.width) / 2
self.imageEdgeInsets = UIEdgeInsets.init(top: imageTop, left: imageLeft, bottom: -imageTop, right: 0)
self.titleEdgeInsets = UIEdgeInsets(top: titleTop, left: -imageSize.width, bottom: -titleTop, right: 0)
} else {
self.imageEdgeInsets = UIEdgeInsets(top: imageTop, left: titleSize.width/2, bottom: -imageTop, right: -titleSize.width/2)
self.titleEdgeInsets = UIEdgeInsets(top: titleTop, left: -imageSize.width/2, bottom: -titleTop, right: imageSize.width/2)
}
完整代碼
最終設(shè)置方法的完整代碼如下,不用命名空間直接寫在擴(kuò)展中的朋友把代碼中的base換成self就可以了
extension YXKit where Base: UIButton {
/*
UIButton默認(rèn)布局是圖片在左、文字在右,圖片和文字之間邊距為0,圖片和文字整體居中顯示
當(dāng)同時(shí)存在image和title時(shí),imageEdgeInsets中的top、left、bottom相對(duì)于UIButton,right相對(duì)于title,同理,titleEdgeInsets中的top、bottom、rright相對(duì)于UIButton,left相對(duì)于title
imageEdgeInsets = UIEdgeInsets.init(top: 5, left: 0, bottom: -5, right: 0) 表示圖片整體向下移動(dòng)5
imageEdgeInsets = UIEdgeInsets.init(top: -5, left: 4, bottom: 5, right: -4) 表示圖片整體向上移動(dòng)5,向右移動(dòng)4
默認(rèn)的情況下當(dāng)按鈕比較小時(shí)會(huì)自動(dòng)保留圖片的尺寸和將文字部分縮小,一般出現(xiàn)在圖標(biāo)和文字上下布局,button的整體較小時(shí),因?yàn)榘粹o總體寬度比較小,導(dǎo)致系統(tǒng)給分配的寬度不足以完整顯示文字
*/
enum ImagePosition: Int {
case top = 1
case left = 2
case bottom = 3
case right = 4
}
/// 設(shè)置label相對(duì)于圖片的位置
/// - Parameters:
/// - anImage: 按鈕圖片
/// - title: 標(biāo)題
/// - imagePosition: label相對(duì)于圖片的位置(上下左右)
/// - additionalSpacing: 文字和圖片的間隔
/// - state: UIControl.State
/// - isSureTitleCompress: 是否明確文字被系統(tǒng)擠壓,true:使用文字被壓縮的調(diào)整模式,false:根據(jù)系統(tǒng)為文字分配的size自動(dòng)適配(主要是了為了應(yīng)對(duì)有些文字?jǐn)D壓的按鈕被重復(fù)設(shè)置的情況)
func setImage(image anImage: UIImage?, title: String, imagePosition: ImagePosition, additionalSpacing: CGFloat, state: UIControl.State = .normal, isSureTitleCompress: Bool = false){
base.setImage(anImage, for: state)
base.setTitle(title, for: state)
positionLabelRespectToImage(title: title, position: imagePosition, spacing: additionalSpacing, isSureTitleCompress: isSureTitleCompress)
}
private func positionLabelRespectToImage(title: String, position: ImagePosition,spacing: CGFloat, isSureTitleCompress: Bool = false) {
base.layoutIfNeeded()//這一步很重要,否則如果UIButton通過約束布局會(huì)導(dǎo)致titleRect獲取的rect不準(zhǔn)
let imageSize = base.imageView?.image?.size ?? .zero
let titleSize = base.titleRect(forContentRect: base.frame).size//系統(tǒng)為titleLabel分配的size
var titleNeedSize: CGSize = .zero//展示文字實(shí)際所需的size
if let font = base.titleLabel?.font {
titleNeedSize = title.size(withAttributes: [NSAttributedString.Key.font: font])
}
var isTitleCompress = false//文字是否被系統(tǒng)壓縮
if isSureTitleCompress {
isTitleCompress = true
} else if titleNeedSize.width > titleSize.width {
isTitleCompress = true
}
switch (position){
case .top:
let imageTop = -(titleSize.height/2 + spacing/2)
let titleTop = imageSize.height/2 + spacing/2
if isTitleCompress {
let imageLeft = (base.bounds.size.width - imageSize.width) / 2
base.imageEdgeInsets = UIEdgeInsets.init(top: imageTop, left: imageLeft, bottom: -imageTop, right: 0)
base.titleEdgeInsets = UIEdgeInsets(top: titleTop, left: -imageSize.width, bottom: -titleTop, right: 0)
} else {
base.imageEdgeInsets = UIEdgeInsets(top: imageTop, left: titleSize.width/2, bottom: -imageTop, right: -titleSize.width/2)
base.titleEdgeInsets = UIEdgeInsets(top: titleTop, left: -imageSize.width/2, bottom: -titleTop, right: imageSize.width/2)
}
case .left:
base.imageEdgeInsets = UIEdgeInsets(top: 0, left: -spacing/2, bottom: 0, right: spacing/2)
base.titleEdgeInsets = UIEdgeInsets(top: 0, left: spacing/2, bottom: 0, right: -spacing/2)
case .bottom:
let imageTop = titleSize.height/2 + spacing/2
let titleTop = -(imageSize.height/2 + spacing/2)
if isTitleCompress {
let imageLeft = (base.bounds.size.width - imageSize.width) / 2
base.imageEdgeInsets = UIEdgeInsets(top: imageTop, left: imageLeft, bottom: -imageTop, right: 0)
base.titleEdgeInsets = UIEdgeInsets(top: titleTop,
left: -imageSize.width, bottom: -titleTop, right: 0)
} else {
base.imageEdgeInsets = UIEdgeInsets(top: imageTop, left: titleSize.width/2, bottom: -imageTop, right: -titleSize.width/2)
base.titleEdgeInsets = UIEdgeInsets(top: titleTop,
left: -imageSize.width/2, bottom: -titleTop, right: imageSize.width/2)
}
case .right:
base.imageEdgeInsets = UIEdgeInsets(top: 0, left: titleSize.width + spacing/2, bottom: 0,
right: -(titleSize.width + spacing/2))
base.titleEdgeInsets = UIEdgeInsets(top: 0, left: -(imageSize.width + spacing/2), bottom: 0, right: imageSize.width + spacing/2)
}
}
}
為什么不考慮高度不夠的情況?因?yàn)閷?shí)際需求中不存在