圓角(RounderCorner)是一種很常見的檢視效果,相比於直角,它更加柔和優美,易於接受。但很多人並不清楚如何設定圓角的正確方式和原理。設定圓角會帶來一定的效能損耗,如何提高效能是另一個需要重點討論的話題。我查閱了一些現有的資料,收穫良多的同時也發現了一些誤導人錯誤。本文總結整理了一些知識點,概括如下:
- 設定圓角的正確姿勢及其原理
- 設定圓角的效能損耗
- 其他設定圓角的方法,以及最優選擇
我為本文製作了一個 demo,讀者可以在我的 github 上 clone 下來:CornerRadius,如果覺得有幫助還望給個star以示支援。專案由 Swift 實現,但請務必相信我即使你只會 Objective-C,也可以看懂它。因為其中的關鍵知識與 Swift 無關。
正確姿勢
首先,我想要宣告的一點是:
設定圓角很簡單,它不會帶來任何效能損耗
因為這件事本來就很簡單,它只需要一行程式碼:
1 |
view.layer.cornerRadius = 5 |
先別急著關掉網頁,也別急著回覆,我們讓事實說話。開啟 Instuments,選擇 Core Animation 除錯,你會發現既沒有 Off-Screen Render,也沒有降低幀數。關於使用 Instuments 分析應用,你可以參考我的這篇文章:UIKit效能調優實戰講解。從截圖中可以看到第三個棕色檢視確確實實設定了圓角:
不過檢視一下程式碼可以發現,有一個 UILabel
也設定了圓角,但是沒有表現出任何變化。關於這一點,你可以檢視 cornerRadius
屬性的註釋:
By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.
也就是說在預設情況下,這個屬性只會影響檢視的背景顏色和 border。對於 UILabel
這樣內部還有子檢視的控制元件就無能為力了。所以很多情況下我們會看到這樣的程式碼:
1 2 |
label.layer.cornerRadius = 5 label.layer.masksToBounds = true |
我們把第二行程式碼新增到 CustomTableViewCell
的構造方法中,再次執行 Instument,就可以看到圓角效果了。
效能損耗
如果你勾選上 Color Offscreen-Rendered Yellow,就會發現 label 的四周出現了黃色的標記,說明這裡出現了離屏渲染。關於離屏渲染的介紹,同樣可以參考:UIKit效能調優實戰講解,就不在本文贅述了。
需要強調的一點是,離屏渲染並非由設定圓角導致的!通過控制變數的方法很容易得出這個結論,因為 UIView 只是設定了 cornerRadius
,但它沒有出現離屏渲染。某些比較權威的文章,比如 Stackoverflow 和 CodeReview 都提到設定 cornerRadius
會導致離屏渲染從而影響效能,我想這實在是冤枉了可愛的 cornerRadius
變數,也誤導了別人。
雖然設定 masksToBounds
會導致離屏渲染,從而影響效能,但是這個影響到底會有多大?在我的 iPhone6 上,即使出現了 17 個帶有圓角的檢視,滑動時的幀數依然在 58 – 59 fps 左右波動。
然而,這並非說明 iOS 9 做了什麼特殊優化,或者是離屏渲染的影響不大,其主要原因在於圓角不夠多。當我將一個 UIImageView
也設定成圓角,也就是螢幕上的圓角檢視達到 34 個時,fps 大幅度下降,大約只有 33 左右。基本上已經達到了影響使用者體驗的範圍。因此,一切不講依據的優化都是耍流氓,如果你的圓角檢視不多,cell 不復雜,就不要費力氣折騰了。
高效地設定圓角
假設現在圓角檢視非常多(比如在 UICollectionView 中),那麼如何為檢視高效的新增圓角呢?網上的教程大多沒有說全,因為這個事要分兩種情況考慮。為普通的 UIView
設定圓角,和為 UIImageView
設定圓角的原理截然不同。
有一種做法是這樣的,這種寫法試圖實現 cornerRadius = 3
的效果:
1 2 3 4 5 6 7 8 9 |
override func drawRect(rect: CGRect) { let maskPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .AllCorners, cornerRadii: CGSize(width: 3, height: 3)) let maskLayer = CAShapeLayer() maskLayer.frame = self.bounds maskLayer.path = maskPath.CGPath self.layer.mask = maskLayer } |
不過這是一種錯的離譜的寫法!
首先,我們應該儘量避免重寫 drawRect
方法。不恰當的使用這個方法會導致記憶體暴增。舉個例子,iPhone6 上與螢幕等大的 UIView
,即使重寫一個空的 drawRect
方法,它也至少佔用 750 * 1134 * 4 位元組 ≈ 3.4 Mb
的記憶體。在 記憶體惡鬼drawRect 及其後續中,作者詳細介紹了其中原理,據他測試,在 iPhone6 上空的、與螢幕等大的檢視重寫 drawRect
方法會消耗 5.2 Mb 記憶體。總之,能避免重寫 drawRect
方法就儘可能避免。
其次,這種方法本質上是用遮罩層 mask
來實現,因此同樣無可避免的會導致離屏渲染。我試著將此前 34 個檢視的圓角改用這種方法實現,結果 fps 掉到 11 左右。已經屬於卡出翔的節奏了。
忘掉這種寫法吧,下面介紹正確的高效設定圓角的姿勢。
為 UIView 新增圓角
這種做法的原理是手動畫出圓角。雖然我們之前說過,為普通的檢視直接設定 cornerRadius
屬性即可。但萬一不可避免的需要使用 masksToBounds
,就可以使用下面這種方法,它的核心程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func kt_drawRectWithRoundedCorner(radius radius: CGFloat, borderWidth: CGFloat, backgroundColor: UIColor, borderColor: UIColor) -> UIImage { UIGraphicsBeginImageContextWithOptions(sizeToFit, false, UIScreen.mainScreen().scale) let context = UIGraphicsGetCurrentContext() CGContextMoveToPoint(context, 開始位置); // 開始座標右邊開始 CGContextAddArcToPoint(context, x1, y1, x2, y2, radius); // 這種型別的程式碼重複四次 CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke) let output = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return output } |
這個方法返回的是 UIImage
,也就是說我們利用 Core Graphics 自己畫出了一個圓角矩形。除了一些必要的程式碼外,最核心的就是 CGContextAddArcToPoint
函式。它中間的四個參數列示曲線的起點和終點座標,最後一個參數列示半徑。呼叫了四次函式後,就可以畫出圓角矩形。最後再從當前的繪圖上下文中獲取圖片並返回。
有了這個圖片後,我們建立一個 UIImageView
並插入到檢視層級的底部:
1 2 3 4 5 6 7 8 9 10 11 12 |
extension UIView { func kt_addCorner(radius radius: CGFloat, borderWidth: CGFloat, backgroundColor: UIColor, borderColor: UIColor) { let imageView = UIImageView(image: kt_drawRectWithRoundedCorner(radius: radius, borderWidth: borderWidth, backgroundColor: backgroundColor, borderColor: borderColor)) self.insertSubview(imageView, atIndex: 0) } } |
完整的程式碼可以在專案中找到,使用時,你只需要這樣寫:
1 2 |
let view = UIView(frame: CGRectMake(1,2,3,4)) view.kt_addCorner(radius: 6) |
為 UIImageView 新增圓角
相比於上面一種實現方法,為 UIImageView
新增圓角更為常用。它的實現思路是直接擷取圖片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
extension UIImage { func kt_drawRectWithRoundedCorner(radius radius: CGFloat, _ sizetoFit: CGSize) -> UIImage { let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: sizetoFit) UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale) CGContextAddPath(UIGraphicsGetCurrentContext(), UIBezierPath(roundedRect: rect, byRoundingCorners: UIRectCorner.AllCorners, cornerRadii: CGSize(width: radius, height: radius)).CGPath) CGContextClip(UIGraphicsGetCurrentContext()) self.drawInRect(rect) CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke) let output = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return output } } |
圓角路徑直接用貝塞爾曲線繪製,一個意外的 bonus 是還可以選擇哪幾個角有圓角效果。這個函式的效果是將原來的 UIImage
剪裁出圓角。配合著這函式,我們可以為 UIImageView 擴充一個設定圓角的方法:
1 2 3 4 5 6 7 8 9 10 |
extension UIImageView { /** / !!!只有當 imageView 不為nil 時,呼叫此方法才有效果 :param: radius 圓角半徑 */ override func kt_addCorner(radius radius: CGFloat) { self.image = self.image?.kt_drawRectWithRoundedCorner(radius: radius, self.bounds.size) } } |
完整的程式碼可以在專案中找到,使用時,你只需要這樣寫:
1 2 |
let imageView = let imgView1 = UIImageView(image: UIImage(name: "")) imageView.kt_addCorner(radius: 6) |
提醒
無論使用上面哪種方法,你都需要小心使用背景顏色。因為此時我們沒有設定 masksToBounds
,因此超出圓角的部分依然會被顯示。因此,你不應該再使用背景顏色,可以在繪製圓角矩形時設定填充顏色來達到類似效果。
在為 UIImageView
新增圓角時,請確保 image
屬性不是 nil
,否則這個設定將會無效。
實戰測試
回到 demo 中,測試一下剛剛定義的這兩個設定圓角的方法。首先在 setupContent
方法中把這兩行程式碼的註釋取消掉:
1 2 |
imgView1.kt_addCorner(radius: 5) imgView2.kt_addCorner(radius: 5) |
然後使用自定義的方法為 label 和 view 設定圓角:
1 2 |
view.kt_addCorner(radius: 6) label.kt_addCorner(radius: 6) |
現在,我們不僅成功的新增了圓角效果,同時還保證了效能不受影響:
總結
- 如果能夠只用
cornerRadius
解決問題,就不用優化。 - 如果必須設定
masksToBounds
,可以參考圓角檢視的數量,如果數量較少(一頁只有幾個)也可以考慮不用優化。 UIImageView
的圓角通過直接擷取圖片實現,其它檢視的圓角可以通過 Core Graphics 畫出圓角矩形實現。
參考資料
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式