[iOS] 高效添加圓角效果

?本文為搬運(yùn)貼,好帖子還是怕blog失效,多一份分享 資料保存更長久。

?iOS 高效添加圓角效果實(shí)戰(zhàn)講解

圓角(RounderCorner)是一種很常見的視圖效果,相比于直角,它更加柔和優(yōu)美,易于接受。但很多人并不清楚如何設(shè)置圓角的正確方式和原理。設(shè)置圓角會帶來一定的性能損耗,如何提高性能是另一個(gè)需要重點(diǎn)討論的話題。我查閱了一些現(xiàn)有的資料,收獲良多的同時(shí)也發(fā)現(xiàn)了一些誤導(dǎo)人錯(cuò)誤。本文總結(jié)整理了一些知識點(diǎn),概括如下:

設(shè)置圓角的正確姿勢及其原理

設(shè)置圓角的性能損耗

其他設(shè)置圓角的方法,以及最優(yōu)選擇

我為本文制作了一個(gè) demo,讀者可以在我的 github 上 clone 下來:CornerRadius,如果覺得有幫助還望給個(gè)star以示支持。項(xiàng)目由 Swift 實(shí)現(xiàn),但請務(wù)必相信我即使你只會 Objective-C,也可以看懂它。因?yàn)槠渲械年P(guān)鍵知識與 Swift 無關(guān)。

正確姿勢:

首先,我想要聲明的一點(diǎn)是:

設(shè)置圓角很簡單,它不會帶來任何性能損耗

因?yàn)檫@件事本來就很簡單,它只需要一行代碼:

view.layer.cornerRadius =5

先別急著關(guān)掉網(wǎng)頁,也別急著回復(fù),我們讓事實(shí)說話。打開 Instuments,選擇?Core Animation調(diào)試,你會發(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è)屬性只會影響視圖的背景顏色和 border。對于?UILabel?這樣內(nèi)部還有子視圖的控件就無能為力了。所以很多情況下我們會看到這樣的代碼:

label.layer.cornerRadius = 5

label.layer.masksToBounds =true

我們把第二行代碼添加到?CustomTableViewCell?的構(gòu)造方法中,再次運(yùn)行 Instument,就可以看到圓角效果了。

性能損耗

如果你勾選上?Color Offscreen-Rendered Yellow,就會發(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?會導(dǎo)致離屏渲染從而影響性能,我想這實(shí)在是冤枉了可愛的?cornerRadius?變量,也誤導(dǎo)了別人。

雖然設(shè)置?masksToBounds?會導(dǎo)致離屏渲染,從而影響性能,但是這個(gè)影響到底會有多大?在我的 iPhone6 上,即使出現(xiàn)了 17 個(gè)帶有圓角的視圖,滑動時(shí)的幀數(shù)依然在 58 - 59 fps 左右波動。

然而,這并非說明 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è)方法會導(dǎo)致內(nèi)存暴增。舉個(gè)例子,iPhone6 上與屏幕等大的?UIView,即使重寫一個(gè)空的?drawRect?方法,它也至少占用?750 * 1134 * 4 字節(jié) ≈ 3.4 Mb?的內(nèi)存。在?內(nèi)存惡鬼drawRect?及其后續(xù)中,作者詳細(xì)介紹了其中原理,據(jù)他測試,在 iPhone6 上空的、與屏幕等大的視圖重寫?drawRect?方法會消耗 5.2 Mb 內(nèi)存??傊?,能避免重寫?drawRect?方法就盡可能避免。

其次,這種方法本質(zhì)上是用遮罩層?mask?來實(shí)現(xiàn),因此同樣無可避免的會導(dǎo)致離屏渲染。我試著將此前 34 個(gè)視圖的圓角改用這種方法實(shí)現(xiàn),結(jié)果 fps 掉到 11 左右。已經(jīng)屬于卡出翔的節(jié)奏了。

忘掉這種寫法吧,下面介紹正確的高效設(shè)置圓角的姿勢。

為 UIView 添加圓角

這種做法的原理是手動畫出圓角。雖然我們之前說過,為普通的視圖直接設(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?并插入到視圖層級的底部:

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 = UIImageView(image:UIImage(name:""))??

imageView.kt_addCorner(radius:6)

提醒

無論使用上面哪種方法,你都需要小心使用背景顏色。因?yàn)榇藭r(shí)我們沒有設(shè)置?masksToBounds,因此超出圓角的部分依然會被顯示。因此,你不應(yīng)該再使用背景顏色,可以在繪制圓角矩形時(shí)設(shè)置填充顏色來達(dá)到類似效果。

在為?UIImageView?添加圓角時(shí),請確保?image?屬性不是?nil,否則這個(gè)設(shè)置將會無效。

實(shí)戰(zhàn)測試

回到 demo 中,測試一下剛剛定義的這兩個(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í)還保證了性能不受影響:

總結(jié)

如果能夠只用?cornerRadius?解決問題,就不用優(yōu)化。

如果必須設(shè)置?masksToBounds,可以參考圓角視圖的數(shù)量,如果數(shù)量較少(一頁只有幾個(gè))也可以考慮不用優(yōu)化。

UIImageView?的圓角通過直接截取圖片實(shí)現(xiàn),其它視圖的圓角可以通過 Core Graphics 畫出圓角矩形實(shí)現(xiàn)。

原文 完

原文地址:https://bestswifter.com/efficient-rounded-corner/


另外插一句,要學(xué)習(xí)一下離屏渲染的話,附上一個(gè)推薦鏈接:http://foggry.com/blog/2015/05/06/chi-ping-xuan-ran-xue-xi-bi-ji/?utm_source=tuicool

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容