demo地址: SPButton
前言
最近我竟花了幾天的時間去深入研究button,研究的過程當中,被imageEdgeInsets
和titleEdgeInsets
兩個屬性困惑甚久,我為此徹夜不眠,網上也查閱各種資料,可以說,對于這兩個屬性的解釋,網上的答案滿天飛,但是,沒有一個人真正說出了它們的原理。
重要關聯屬性contentHorizontalAlignment和contentVerticalAlignment
這是兩個枚舉,即整個內容的水平對齊方式和垂直對齊方式
typedef NS_ENUM(NSInteger, UIControlContentHorizontalAlignment) {
UIControlContentHorizontalAlignmentCenter = 0,
UIControlContentHorizontalAlignmentLeft = 1,
UIControlContentHorizontalAlignmentRight = 2,
UIControlContentHorizontalAlignmentFill = 3,
UIControlContentHorizontalAlignmentLeading API_AVAILABLE(ios(11.0), tvos(11.0)) = 4,
UIControlContentHorizontalAlignmentTrailing API_AVAILABLE(ios(11.0), tvos(11.0)) = 5,
};
typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
UIControlContentVerticalAlignmentCenter = 0,
UIControlContentVerticalAlignmentTop = 1,
UIControlContentVerticalAlignmentBottom = 2,
UIControlContentVerticalAlignmentFill = 3,
};
// 默認:
button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
button.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
其中UIControlContentHorizontalAlignmentLeading和UIControlContentHorizontalAlignmentTrailing為iOS11新增,在我們大中華地區,Leading就是Left,Trailing就是Right ,對于部分國家,他們的語言是從右往左寫,這時Leading就是Right,Trailing就Left
正文
創建一個按鈕,設置文字和圖片,按鈕的內容默認排布如圖:為了便于理解,我給的titleLabel和imageView是等寬的
截圖中:
- 黑色邊框為
按鈕
矩形區域,其bounds為:(0,0,200,100)
,為了便于研究,contentEdgeInset默認UIEdgeInsetsZero,即按鈕的內容區域就是按鈕的bounds; -
imageView
的frame為(50,25,50,50)
; -
titleLabel
的frame為(100,37.5,50,50)
;
現在,我設置
button.imageEdgeInsets = UIEdgeInsetsMake(0,50, 0,0);
經過上面的設置后,請大家猜想一下,圖片的位置會在什么地方?
思考 1s、2s、3s、.......
大家心中差不多有想法了,圖片的原x值為50,現在設置UIEdgeInsetsMake(0,50, 0,0),相當于整個圖片向右平移50,那么現在圖片的x值應該為100,大家想象的結果是不是這樣的,如圖:
我要告訴大家,上面的結果是錯的,
正確的結果
如圖:實際上,圖片只向右平移了50的一半,即25,這是為什么?
網上錯誤結論:
對于imageView:其
imageEdgeInsets
的top,left,bottom是相對button的contentRect
而言,right是相對titleLabel而言;
對于titleLabel:其titleEdgeInsets
的top,right,bottom是相對button的contentRect
而言,left是相對imageView而言。
正確結論
imageEdgeInsets
和titleEdgeInsets
的top,left,right, bottom都是相對button的contentRect
而言,當contentEdgeInsets為UIEdgeInsetsZero時,button、imageView、titleLabel的安全區域均為button的bounds。
根據這個正確結論,當設置了button.imageEdgeInsets = UIEdgeInsetsMake(0,50, 0,0)
時,那么imageView的安全區域就是如下圖中的紅色區域
圖片的區域我們知道了,根據水平排列方式默認為UIControlContentHorizontalAlignmentCenter
,圖片應當在紅色區域的中間位置,然而,我們要深刻明白:
因此,盡管titleLabel沒有設置titleEdgeInsets,但是我們在對imageView進行某種對齊時,不應該只考慮imageView,應該將imageView+titleLabel這個整體作為考慮對象; 如圖重要的話說3遍
- UIControlContentHorizontalAlignmentCenter的指的是內容(圖片+文字)整體居中
- UIControlContentHorizontalAlignmentCenter的指的是內容(圖片+文字)整體居中
- UIControlContentHorizontalAlignmentCenter的指的是內容(圖片+文字)整體居中
其余枚舉值同理
核心解釋
上圖中,imageView和藍色的titleLabel作為一個整體,在紅色區域內居中了,綠色的titleLabel只參與計算,由于我們沒有設置titleLabel的titleEdgeInsets,所以最終titleLabel的位置依然保持不變。藍色的titleLabel實際上是虛擬的,我只是告訴大家,系統進行對齊方式計算時,永遠是把imageView+titleLabel這個整體作為計算對象,我們來計算一下,圖片向右偏移25是怎么來的:
①紅色區域的寬度為:200 - 50 = 150;
②圖片+藍色label的總寬度:50 + 50 = 100;
③圖片的x值:(① - ②) / 2.0 =(150 - 100)/ 2.0 = 25;(除以2是因為居中對齊,如果是其余對齊就不用除以2)
我不知道我上面的表達夠不夠清楚,如果不清楚,那么我們來一次強化訓練
強化訓練
我們不再按照水平中心對齊,我們來一次左對齊
button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
設置后如圖
再設置
button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 50);
大家想想經過上面那行代碼之后,結果是什么呢?圖片會向左偏移50的距離嗎?如果按照網上的結論,圖片的right是相對titleLabel而言,那么設置right為50圖片必會向左偏移50。我要告訴大家,上面那行代碼設置之后,不會產生任何變化,為什么?
在這個紅色區域當中,將imageView+(虛擬)titleLabel這個整體進行左對齊,大家明顯能看到,現在就是左對齊的,所以設置right為50是不會有任何變化的,那么如果我們修改一下,設置
button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 175);
上面那行代碼的意思是,圖片的安全區域為:在contentRect的基礎上,原區域右邊往左內縮175距離,即下圖中的紅色區域:在這個紅色區域內,要把imageView+(虛擬)titleLabel這個整體進行左對齊,但是我們發現,紅色區域的寬度容不下imageView+titleLabel這個整體,這個時候,系統先會把titleLabel的寬度壓縮,如果壓縮為0之后,發現連imageView都容不下,那么繼續壓縮imageView,直到寬度降為紅色區域寬為止,titleLabel保持不動, 最終顯示結果如圖
再次訓練
保持默認設置
button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
button.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
再設置
button.imageEdgeInsets = UIEdgeInsetsMake(50, 0, 0, 0);
*上面那行代碼的意思是,圖片的安全區域為:在contentRect的基礎上,原區域頂部向下內縮50距離,即下圖中的紅色區域:在這個紅色區域當中,要依然保證imageView+(虛擬)titleLabel這個整體進行垂直居中, 因此最終結果如圖:
從這里我們可以萌生一個思想
imageEdgeInsets
和titleEdgeInsets
不要去理解為將imageView和titleLabel進行平移,應該理解為將imageView和titleLabel的安全區域的各邊進行偏移,偏移完成后,再聯合contentHorizontalAlignment
和contentVerticalAlignment
屬性進行整體對齊
我所知道的秘密
我想大家在實現按鈕圖片位置在上、下、左、右的需求時,有不少人是通過重寫按鈕的imageRectForContentRect:
和titleRectForContentRect:
的,我個人也很推薦這種做法,重寫layoutSubviews
也可以,但我并不推薦,可以說重寫layoutSubviews可以實現你的需求,但是嚴重破壞了系統按鈕,因為,系統按鈕在layoutSubviews里面,當存在文字或者圖片時,會先調用imageRectForContentRect:
和titleRectForContentRect:
這2個方法計算出imageRect和titleRect,然后將計算結果應用在imageView和titleLabel上,所以,如果你重寫layoutSubviews,先super , 然后進行一系列自己的布局,這就會導致你使用button時,通過imageRectForContentRect:
和titleRectForContentRect:
這2個方法獲取到的rect并非你在layoutSubviews里計算的結果,仍然是系統計算的結果,這就是破壞了原始按鈕的方法
-
imageRectForContentRect:
和titleRectForContentRect:
的調用時機:
- 在第一次調用titleLabel和imageView的getter方法(懶加載)時,alloc init之前會調用一次(無論有無圖片文字都會直接調),因此,在重寫這2個方法時,在方法里面不要使用self.imageView和self.titleLabel,因為這2個控件是懶加載,如果在重寫的這2個方法里是第一調用imageView和titleLabel的getter方法, 則會造成死循環
- 在layoutsSubviews中如果文字或圖片不為空時會調用, 測試方式:在重寫的這兩個方法里調用setNeedsLayout(layutSubviews),發現會造成死循環
- 按鈕的frame發生改變,設置文字圖片、改動文字和圖片、設置對齊方式,設置內容區域等時會調用,其實這些,系統是調用了layoutSubviews從而間接的去調用
imageRectForContentRect:
和titleRectForContentRect:
......
建議
大家在實現按鈕的圖片在上、左、下、右的時候,最好要注意不要去破壞系統按鈕,什么叫破壞呢?比如你實現完之后,要保證按鈕的所有自帶屬性和方法依然生效,再比如:UIButton中的titleLabel和imageView是懶加載的,我們不要在實現自己需求的過程中去提前加載,這不符合按鈕的規則
demo地址: SPButton