iOS 高效添加圓角效果實(shí)戰(zhàn)講解
圓角(RounderCorner)是一種很常見的視圖效果,相比于直角,它更加柔和優(yōu)美,易于接受。但很多人并不清楚如何設(shè)置圓角的正確方式和原理。設(shè)置圓角會(huì)帶來一定的性能損耗,如何提高性能是另一個(gè)需要重點(diǎn)討論的話題。我查閱了一些現(xiàn)有的資料,收獲良多的同時(shí)也發(fā)現(xiàn)了一些誤導(dǎo)人錯(cuò)誤。本文總結(jié)整理了一些知識(shí)點(diǎn),概括如下:
- 設(shè)置圓角的正確姿勢(shì)及其原理
- 設(shè)置圓角的性能損耗
- 其他設(shè)置圓角的方法,以及最優(yōu)選擇
我為本文制作了一個(gè) demo,讀者可以在我的 github 上 clone 下來:CornerRadius,如果覺得有幫助還望給個(gè)star以示支持。項(xiàng)目由 Swift 實(shí)現(xiàn),但請(qǐng)務(wù)必相信我即使你只會(huì) Objective-C,也可以看懂它。因?yàn)槠渲械年P(guān)鍵知識(shí)與 Swift 無關(guān)。
我為本文制作了一個(gè) demo,讀者可以在我的 github 上 clone 下來:CornerRadius,如果覺得有幫助還望給個(gè)star以示支持。項(xiàng)目由 Swift 實(shí)現(xiàn),但請(qǐng)務(wù)必相信我即使你只會(huì) Objective-C,也可以看懂它。因?yàn)槠渲械年P(guān)鍵知識(shí)與 Swift 無關(guān)。
正確姿勢(shì)
首先,我想要聲明的一點(diǎn)是:設(shè)置圓角很簡(jiǎn)單,它不會(huì)帶來任何性能損耗。
因?yàn)檫@件事本來就很簡(jiǎn)單,它只需要一行代碼:
- view.layer.cornerRadius = 5
先別急著關(guān)掉網(wǎng)頁,也別急著回復(fù),我們讓事實(shí)說話。打開 Instuments,選擇 Core Animation 調(diào)試,你會(huì)發(fā)現(xiàn)既沒有 Off-Screen Render,也沒有降低幀數(shù)。關(guān)于使用 Instuments 分析應(yīng)用,你可以參考我的這篇文章:UIKit性能調(diào)優(yōu)實(shí)戰(zhàn)講解。從截圖中可以看到第三個(gè)棕色視圖確確實(shí)實(shí)設(shè)置了圓角:
不過查看一下代碼可以發(fā)現(xiàn),有一個(gè) UILabel 也設(shè)置了圓角,但是沒有表現(xiàn)出任何變化。關(guān)于這一點(diǎn),你可以查看 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.
也就是說在默認(rèn)情況下,這個(gè)屬性只會(huì)影響視圖的背景顏色和 border。對(duì)于 UILabel 這樣內(nèi)部還有子視圖的控件就無能為力了。所以很多情況下我們會(huì)看到這樣的代碼:
- label.layer.cornerRadius = 5
- label.layer.masksToBounds = true
我們把第二行代碼添加到 CustomTableViewCell 的構(gòu)造方法中,再次運(yùn)行 Instument,就可以看到圓角效果了。
性能損耗
如果你勾選上 Color Offscreen-Rendered Yellow,就會(huì)發(fā)現(xiàn) label 的四周出現(xiàn)了黃色的標(biāo)記,說明這里出現(xiàn)了離屏渲染。關(guān)于離屏渲染的介紹,同樣可以參考:UIKit性能調(diào)優(yōu)實(shí)戰(zhàn)講解,就不在本文贅述了。
需要強(qiáng)調(diào)的一點(diǎn)是,離屏渲染并非由設(shè)置圓角導(dǎo)致的!通過控制變量的方法很容易得出這個(gè)結(jié)論,因?yàn)?UIView 只是設(shè)置了 cornerRadius,但它沒有出現(xiàn)離屏渲染。某些比較權(quán)威的文章,比如 Stackoverflow 和 CodeReview 都提到設(shè)置 cornerRadius 會(huì)導(dǎo)致離屏渲染從而影響性能,我想這實(shí)在是冤枉了可愛的 cornerRadius 變量,也誤導(dǎo)了別人。
雖然設(shè)置 masksToBounds 會(huì)導(dǎo)致離屏渲染,從而影響性能,但是這個(gè)影響到底會(huì)有多大?在我的 iPhone6 上,即使出現(xiàn)了 17 個(gè)帶有圓角的視圖,滑動(dòng)時(shí)的幀數(shù)依然在 58 - 59 fps 左右波動(dòng)。
然而,這并非說明 iOS 9 做了什么特殊優(yōu)化,或者是離屏渲染的影響不大,其主要原因在于圓角不夠多。當(dāng)我將一個(gè) UIImageView 也設(shè)置成圓角,也就是屏幕上的圓角視圖達(dá)到 34 個(gè)時(shí),fps 大幅度下降,大約只有 33 左右。基本上已經(jīng)達(dá)到了影響用戶體驗(yàn)的范圍。因此,一切不講依據(jù)的優(yōu)化都是耍流氓,如果你的圓角視圖不多,cell 不復(fù)雜,就不要費(fèi)力氣折騰了。
高效地設(shè)置圓角
假設(shè)現(xiàn)在圓角視圖非常多(比如在 UICollectionView 中),那么如何為視圖高效的添加圓角呢?網(wǎng)上的教程大多沒有說全,因?yàn)檫@個(gè)事要分兩種情況考慮。為普通的 UIView 設(shè)置圓角,和為 UIImageView 設(shè)置圓角的原理截然不同。
有一種做法是這樣的,這種寫法試圖實(shí)現(xiàn) cornerRadius = 3 的效果:
- 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
- }
不過這是一種錯(cuò)的離譜的寫法!
首先,我們應(yīng)該盡量避免重寫 drawRect 方法。不恰當(dāng)?shù)氖褂眠@個(gè)方法會(huì)導(dǎo)致內(nèi)存暴增。舉個(gè)例子,iPhone6 上與屏幕等大的 UIView,即使重寫一個(gè)空的 drawRect 方法,它也至少占用 750 * 1134 * 4 字節(jié) ≈ 3.4 Mb 的內(nèi)存。在內(nèi)存惡鬼drawRect 及其后續(xù)中,作者詳細(xì)介紹了其中原理,據(jù)他測(cè)試,在 iPhone6 上空的、與屏幕等大的視圖重寫 drawRect 方法會(huì)消耗 5.2 Mb 內(nèi)存。總之,能避免重寫 drawRect 方法就盡可能避免。
其次,這種方法本質(zhì)上是用遮罩層 mask 來實(shí)現(xiàn),因此同樣無可避免的會(huì)導(dǎo)致離屏渲染。我試著將此前 34 個(gè)視圖的圓角改用這種方法實(shí)現(xiàn),結(jié)果 fps 掉到 11 左右。已經(jīng)屬于卡出翔的節(jié)奏了。
忘掉這種寫法吧,下面介紹正確的高效設(shè)置圓角的姿勢(shì)。
為 UIView 添加圓角
這種做法的原理是手動(dòng)畫出圓角。雖然我們之前說過,為普通的視圖直接設(shè)置 cornerRadius 屬性即可。但萬一不可避免的需要使用 masksToBounds,就可以使用下面這種方法,它的核心代碼如下:
- func kt_drawRectWithRoundedCorner(radius radius: CGFloat,
- borderWidth: CGFloat,
- backgroundColor: UIColor,
- borderColor: UIColor) -> UIImage {
- UIGraphicsBeginImageContextWithOptions(sizeToFit, false, UIScreen.mainScreen().scale)
- let context = UIGraphicsGetCurrentContext()
- CGContextMoveToPoint(context, 開始位置); // 開始坐標(biāo)右邊開始
- CGContextAddArcToPoint(context, x1, y1, x2, y2, radius); // 這種類型的代碼重復(fù)四次
- CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
- let output = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
- return output
- }
這個(gè)方法返回的是 UIImage,也就是說我們利用 Core Graphics 自己畫出了一個(gè)圓角矩形。除了一些必要的代碼外,最核心的就是 CGContextAddArcToPoint 函數(shù)。它中間的四個(gè)參數(shù)表示曲線的起點(diǎn)和終點(diǎn)坐標(biāo),最后一個(gè)參數(shù)表示半徑。調(diào)用了四次函數(shù)后,就可以畫出圓角矩形。最后再從當(dāng)前的繪圖上下文中獲取圖片并返回。
有了這個(gè)圖片后,我們創(chuàng)建一個(gè) UIImageView 并插入到視圖層級(jí)的底部:
- 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)
- }
- }
完整的代碼可以在項(xiàng)目中找到,使用時(shí),你只需要這樣寫:
- let view = UIView(frame: CGRectMake(1,2,3,4))
- view.kt_addCorner(radius: 6)
為 UIImageView 添加圓角
相比于上面一種實(shí)現(xiàn)方法,為 UIImageView 添加圓角更為常用。它的實(shí)現(xiàn)思路是直接截取圖片:
- 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
- }
- }
圓角路徑直接用貝塞爾曲線繪制,一個(gè)意外的 bonus 是還可以選擇哪幾個(gè)角有圓角效果。這個(gè)函數(shù)的效果是將原來的 UIImage 剪裁出圓角。配合著這函數(shù),我們可以為 UIImageView 拓展一個(gè)設(shè)置圓角的方法:
- extension UIImageView {
- /**
- / !!!只有當(dāng) imageView 不為nil 時(shí),調(diào)用此方法才有效果
- :param: radius 圓角半徑
- */
- override func kt_addCorner(radius radius: CGFloat) {
- self.image = self.image?.kt_drawRectWithRoundedCorner(radius: radius, self.bounds.size)
- }
- }
完整的代碼可以在項(xiàng)目中找到,使用時(shí),你只需要這樣寫:
- let imageView = let imgView1 = UIImageView(image: UIImage(name: ""))
- imageView.kt_addCorner(radius: 6)
提醒:
無論使用上面哪種方法,你都需要小心使用背景顏色。因?yàn)榇藭r(shí)我們沒有設(shè)置 masksToBounds,因此超出圓角的部分依然會(huì)被顯示。因此,你不應(yīng)該再使用背景顏色,可以在繪制圓角矩形時(shí)設(shè)置填充顏色來達(dá)到類似效果。
在為 UIImageView 添加圓角時(shí),請(qǐng)確保 image 屬性不是 nil,否則這個(gè)設(shè)置將會(huì)無效。
實(shí)戰(zhàn)測(cè)試
回到 demo 中,測(cè)試一下剛剛定義的這兩個(gè)設(shè)置圓角的方法。首先在 setupContent 方法中把這兩行代碼的注釋取消掉:
- imgView1.kt_addCorner(radius: 5)
- imgView2.kt_addCorner(radius: 5)
然后使用自定義的方法為 label 和 view 設(shè)置圓角:
- view.kt_addCorner(radius: 6)
- label.kt_addCorner(radius: 6)
現(xiàn)在,我們不僅成功的添加了圓角效果,同時(shí)還保證了性能不受影響:
性能測(cè)試
總結(jié)
- 如果能夠只用 cornerRadius 解決問題,就不用優(yōu)化。
- 如果必須設(shè)置 masksToBounds,可以參考圓角視圖的數(shù)量,如果數(shù)量較少(一頁只有幾個(gè))也可以考慮不用優(yōu)化。
- UIImageView 的圓角通過直接截取圖片實(shí)現(xiàn),其它視圖的圓角可以通過 Core Graphics 畫出圓角矩形實(shí)現(xiàn)。
參考資料