理論
var imageEdgeInsets: UIEdgeInsets { get set }
Use this property to resize and reposition the effective drawing rectangle for the button image. You can specify a different value for each of the four insets (top, left, bottom, right). 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. Use the UIEdgeInsetsMake(::::) function to construct a value for this property. The default value is UIEdgeInsetsZero.
var titleEdgeInsets: UIEdgeInsets { get set }
Use this property to resize and reposition the effective drawing rectangle for the button title. You can specify a different value for each of the four insets (top, left, bottom, right). 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. Use the UIEdgeInsetsMake(::::) function to construct a value for this property. The default value is UIEdgeInsetsZero.
官方文檔對(duì)于這個(gè)兩個(gè)屬性的解釋如上,老實(shí)說(shuō),非常不直觀,看完依然不懂。要理解titleEdgeInsets,可以從以下幾方面考慮(imageEdgeInsets的理解與titleEdgeInsets完全一致):
- 首先,最重要的是將它理解為title的新位置與title舊位置之間的相對(duì)關(guān)系!而不是title與button中心的相對(duì)關(guān)系。
-
其次,要理解正值和負(fù)值對(duì)position的影響,可以參考CSS的padding。在CSS中,padding為正時(shí),意味著view向內(nèi)收縮,padding為負(fù),意味著view向外擴(kuò)張。
借用CSS中Padding的概念,對(duì)于titleEdgeInsets.left,
- 為0時(shí),titleLabel的左側(cè)不動(dòng)
- 為正值時(shí),titleLabel的左側(cè)向內(nèi)收縮,表現(xiàn)出來(lái)的就是→→→(右移)
- 為負(fù)值時(shí),titleLabel的左側(cè)向外擴(kuò)張,表現(xiàn)出來(lái)的就是←←←(左移)
對(duì)于titleEdgeInsets.right,
- 為0時(shí),titleLabel的右側(cè)不動(dòng)
- 為正值時(shí),titleLabel的右側(cè)向內(nèi)收縮,表現(xiàn)出來(lái)的就是←←←(左移)
- 為負(fù)值時(shí),titleLabel的左側(cè)向外擴(kuò)張,表現(xiàn)出來(lái)的就是→→→(右移)
對(duì)于titleEdgeInsets.top,
- 為0時(shí),titleLabel的上側(cè)不動(dòng)
- 為正值時(shí),titleLabel的上側(cè)向內(nèi)收縮,表現(xiàn)出來(lái)的就是↓↓↓(下移)
- 為負(fù)值時(shí),titleLabel的上側(cè)向外擴(kuò)張,表現(xiàn)出來(lái)的就是↑↑↑(上移)
對(duì)于titleEdgeInsets.bottom,
- 為0時(shí),titleLabel的下側(cè)不動(dòng)
- 為正值時(shí),titleLabel的下側(cè)向內(nèi)收縮,表現(xiàn)出來(lái)的就是↑↑↑(上移)
- 為負(fù)值時(shí),titleLabel的下側(cè)向外擴(kuò)張,表現(xiàn)出來(lái)的就是↓↓↓(下移)
實(shí)際使用
當(dāng)button同時(shí)存在image和title時(shí),默認(rèn)image在左,title在右,兩者之間無(wú)空隙(如下圖第一個(gè))。但我們拿到的UI設(shè)計(jì)卻可能是下圖的后三個(gè)。如何實(shí)現(xiàn)后三個(gè)設(shè)計(jì)?最簡(jiǎn)單粗暴的方式就是直接用自定義一個(gè)UIView/UIControl,在上面加上UILable和UIImageView。但比較優(yōu)雅的方式就是配置imageEdgeInsets與titleEdgeInsets。

實(shí)現(xiàn)titleLabel在左,imageView在右
首先不考慮兩者之間的spacing,答案就是
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: titleLabel.width, bottom: 0, right: -titleLabel.width)
button.titleEdgeInsets = UIEdgeInsets(top: 0, left: -imageView.width, bottom: 0, right: imageView.width)
imageView右移,相當(dāng)于它的左側(cè)向內(nèi)收縮了titleLabel.width(正值),它的右側(cè)向外擴(kuò)張了titleLabel.width(負(fù)值)。
titleLabel左移,相當(dāng)于它的左側(cè)向外擴(kuò)張了imageView.width(負(fù)值)它的右側(cè)向內(nèi)收縮了imageView.width(正值)。
實(shí)際使用時(shí),我們可以編寫(xiě)一個(gè)UIButton的extension,方便使用。實(shí)現(xiàn)時(shí)需要考慮title和image之間的spacing,以及增加spacing后若button寬度不夠?qū)е碌膇mage和title的壓縮。下面的代碼給出了alignHorizontal()的兩種實(shí)現(xiàn),第一個(gè)是完全按照我們上面所講概念編寫(xiě)的,第二個(gè)是一種更優(yōu)雅的寫(xiě)法。
extension UIButton {
func alignHorizontal2(spacing: CGFloat, imageFirst: Bool) {
let edgeOffset = spacing / 2
if imageFirst {
imageEdgeInsets = UIEdgeInsets(top: 0,
left: -edgeOffset,
bottom: 0,
right: edgeOffset)
titleEdgeInsets = UIEdgeInsets(top: 0,
left: edgeOffset,
bottom: 0,
right: -edgeOffset)
} else {
guard let imageSize = self.imageView?.image?.size,
let text = self.titleLabel?.text,
let font = self.titleLabel?.font
else {
return
}
let labelString = NSString(string: text)
let titleSize = labelString.size(attributes: [NSFontAttributeName: font])
imageEdgeInsets = UIEdgeInsets(top: 0,
left: titleSize.width + edgeOffset,
bottom: 0,
right: -titleSize.width - edgeOffset)
titleEdgeInsets = UIEdgeInsets(top: 0,
left: -imageSize.width - edgeOffset,
bottom: 0,
right: imageSize.width + edgeOffset)
}
// increase content width to avoid clipping
contentEdgeInsets = UIEdgeInsets(top: 0, left: edgeOffset, bottom: 0, right: edgeOffset)
}
func alignHorizontal(spacing: CGFloat, imageFirst: Bool) {
let edgeOffset = spacing / 2
imageEdgeInsets = UIEdgeInsets(top: 0,
left: -edgeOffset,
bottom: 0,
right: edgeOffset)
titleEdgeInsets = UIEdgeInsets(top: 0,
left: edgeOffset,
bottom: 0,
right: -edgeOffset)
if !imageFirst {
self.transform = CGAffineTransform(scaleX: -1, y: 1)
imageView?.transform = CGAffineTransform(scaleX: -1, y: 1)
titleLabel?.transform = CGAffineTransform(scaleX: -1, y: 1)
}
// increase content width to avoid clipping
contentEdgeInsets = UIEdgeInsets(top: 0, left: edgeOffset, bottom: 0, right: edgeOffset)
}
}
實(shí)現(xiàn)titleLabel和imageView垂直居中
若imageView在上,titleLabel在下,且不考慮兩者之間的spacing,答案是
let imageVerticalOffset = (titleSize.height + spacing)/2
let titleVerticalOffset = (imageSize.height + spacing)/2
let imageHorizontalOffset = (titleSize.width)/2
let titleHorizontalOffset = (imageSize.width)/2
imageEdgeInsets = UIEdgeInsets(top: -imageVerticalOffset,
left: imageHorizontalOffset,
bottom: imageVerticalOffset,
right: -imageHorizontalOffset)
titleEdgeInsets = UIEdgeInsets(top: titleVerticalOffset,
left: -titleHorizontalOffset,
bottom: -titleVerticalOffset,
right: titleHorizontalOffset)
對(duì)于imageView,是右移+上移,以button的中心為原點(diǎn),原imageView中心坐標(biāo)是(titleWidth/2, 0),新imageView中心坐標(biāo)是(0, titleHeight/2),因此水平方向的位移是titleWidth/2,垂直方向上的是titleHeight/2。相當(dāng)于它的左側(cè)向內(nèi)收縮了titleWidth/2(正值),它的右側(cè)向外擴(kuò)張了titleWidth/2(負(fù)值),上側(cè)向上擴(kuò)張了titleHeight/2(負(fù)值),下側(cè)向內(nèi)收縮了titleHeight/2(正值)。
對(duì)于titleLabel,是左移+下移,以button的中心為原點(diǎn),原titleLabel中心坐標(biāo)是(imageWidth/2, 0),新titleLabel中心坐標(biāo)是(0, -imageHeight/2),,因此水平方向的位移是imageWidth/2,垂直方向上的是imageHeight/2。相當(dāng)于它的左側(cè)向外擴(kuò)張了imageWidth/2(負(fù)值),它的右側(cè)向內(nèi)收縮了imageWidth/2(正值),上側(cè)向內(nèi)收縮了imageHeight/2(正值),下側(cè)向外擴(kuò)張了imageHeight/2(負(fù)值)。
實(shí)際使用使用的代碼如下,該代碼考慮title和image之間的spacing,以及增加spacing后若button高度不夠?qū)е碌膇mage和title的壓縮。
extension UIButton {
func alignVertical(spacing: CGFloat, imageTop: Bool) {
guard let imageSize = self.imageView?.image?.size,
let text = self.titleLabel?.text,
let font = self.titleLabel?.font
else {
return
}
let labelString = NSString(string: text)
let titleSize = labelString.size(attributes: [NSFontAttributeName: font])
let imageVerticalOffset = (titleSize.height + spacing)/2
let titleVerticalOffset = (imageSize.height + spacing)/2
let imageHorizontalOffset = (titleSize.width)/2
let titleHorizontalOffset = (imageSize.width)/2
let sign: CGFloat = imageTop ? 1 : -1
imageEdgeInsets = UIEdgeInsets(top: -imageVerticalOffset * sign,
left: imageHorizontalOffset,
bottom: imageVerticalOffset * sign,
right: -imageHorizontalOffset)
titleEdgeInsets = UIEdgeInsets(top: titleVerticalOffset * sign,
left: -titleHorizontalOffset,
bottom: -titleVerticalOffset * sign,
right: titleHorizontalOffset)
// increase content height to avoid clipping
let edgeOffset = (min(imageSize.height, titleSize.height) + spacing)/2
contentEdgeInsets = UIEdgeInsets(top: edgeOffset, left: 0, bottom: edgeOffset, right: 0)
}
}
最好“成對(duì)”EdgeInsets
我們上面講的都是titleEdgeInset的標(biāo)準(zhǔn)用法,即left與right,top與bottom都是成對(duì)設(shè)置,且它們的值互為相反數(shù)。我們最好一直使用這種方式,否則可能導(dǎo)致未知bug。
若僅設(shè)置titleEdgeInsets.left,不設(shè)置titleEdgeInsets.right會(huì)怎樣?
若僅設(shè)置titleEdgeInsets.left = -imageWidth,title會(huì)居中,而不是靠左!即相當(dāng)于titleEdgeInsets.left = -imageWidth/2,titleEdgeInsets.right = -imageWidth/2。
這樣看起來(lái)似乎是一種設(shè)置edgeInsets的簡(jiǎn)便寫(xiě)法,即不需要設(shè)置4個(gè)值,僅需要設(shè)置翻倍的2個(gè)就好了。但最好不要這樣寫(xiě),因?yàn)闇y(cè)試過(guò)程中發(fā)現(xiàn),使用這種方式實(shí)現(xiàn)alignVertical(spacing: CGFloat, imageTop: Bool) 時(shí),若button的高度正好囊括image和title時(shí),imageTop為true或false時(shí),總會(huì)有一種情況下image會(huì)被壓縮。
搜了很多文章也沒(méi)找到titleEdgeInset和imageEdgeInset實(shí)現(xiàn)機(jī)制,所有人都是在猜它是怎么實(shí)現(xiàn)的,因此也無(wú)法確定這個(gè)bug的原因。
若設(shè)置titleEdgeInsets.left == titleEdgeInsets.right會(huì)怎樣?
標(biāo)準(zhǔn)用法中,titleEdgeInsets.left和titleEdgeInsets.right總是互為相反數(shù),因?yàn)楫?dāng)title向一個(gè)方向移動(dòng)時(shí),必然是一側(cè)收縮,一側(cè)擴(kuò)張。那么titleEdgeInsets.left == titleEdgeInsets.right時(shí)會(huì)怎樣呢?測(cè)試顯示結(jié)果不確定。
當(dāng)titleEdgeInsets.left == titleEdgeInsets.right == titleWidth/2時(shí),按理說(shuō)title肯定會(huì)被完全折疊,消失,但實(shí)際上
- 若button寬度正好囊括image和title,title才會(huì)完全折疊起來(lái)。
- 若button寬度比較大時(shí),title只會(huì)部分折疊。
因?yàn)镋dgeInset的機(jī)制未知,所以也無(wú)法猜透它的原因。總之,最好按標(biāo)準(zhǔn)方式操作titleEdgeInset和imageEdgeInset,不然會(huì)有很多莫名其妙的問(wèn)題。
其他
- 使用Xib時(shí),imageEdgeInsets和titleEdgeInsets的right都不能設(shè)置為負(fù)數(shù),經(jīng)測(cè)試,通過(guò)代碼方式設(shè)置負(fù)數(shù)是有效的,Xib中無(wú)法設(shè)置應(yīng)該是Bug。
參考
詳解UIButton的imageEdgeInsets和titleEdgeInsets屬性
UIButton的titleEdgeInsets屬性和imageEdgeInsets屬性實(shí)現(xiàn)圖片文字按要求排列
UIButton: how to center an image and a text using imageEdgeInsets and titleEdgeInsets?