iOS 高效添加圓角效果實戰講解

来源:http://www.cnblogs.com/8hao/archive/2016/03/01/5230362.html
-Advertisement-
Play Games

圓角(RounderCorner)是一種很常見的視圖效果,相比於直角,它更加柔和優美,易於接受。但很多人並不清楚如何設置圓角的正確方式和原理。設置圓角會帶來一定的性能損耗,如何提高性能是另一個需要重點討論的話題。我查閱了一些現有的資料,收穫良多的同時也發現了一些誤導人錯誤。本文總結整理了一些知識點,


圓角(RounderCorner)是一種很常見的視圖效果,相比於直角,它更加柔和優美,易於接受。但很多人並不清楚如何設置圓角的正確方式和原理。設置圓角會帶來一定的性能損耗,如何提高性能是另一個需要重點討論的話題。我查閱了一些現有的資料,收穫良多的同時也發現了一些誤導人錯誤。本文總結整理了一些知識點,概括如下:

  • 設置圓角的正確姿勢及其原理

  • 設置圓角的性能損耗

  • 其他設置圓角的方法,以及最優選擇

我為本文製作了一個 demo,讀者可以在我的 github 上 clone 下來:CornerRadius,如果覺得有幫助還望給個star以示支持。項目由 Swift 實現,但請務必相信我即使你只會 Objective-C,也可以看懂它。因為其中的關鍵知識與 Swift 無關。

我為本文製作了一個 demo,讀者可以在我的 github 上 clone 下來:CornerRadius,如果覺得有幫助還望給個star以示支持。項目由 Swift 實現,但請務必相信我即使你只會 Objective-C,也可以看懂它。因為其中的關鍵知識與 Swift 無關。

正確姿勢

首先,我想要聲明的一點是:設置圓角很簡單,它不會帶來任何性能損耗。

因為這件事本來就很簡單,它只需要一行代碼:

1 view.layer.cornerRadius = 5

先別急著關掉網頁,也別急著回覆,我們讓事實說話。打開 Instuments,選擇 Core Animation 調試,你會發現既沒有 Off-Screen Render,也沒有降低幀數。關於使用 Instuments 分析應用,你可以參考我的這篇文章:UIKit性能調優實戰講解。從截圖中可以看到第三個棕色視圖確確實實設置了圓角:

1171077-ce706c8797fdcdef.jpg

不過查看一下代碼可以發現,有一個 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 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)

現在,我們不僅成功的添加了圓角效果,同時還保證了性能不受影響:

1171077-331ca6074d5b02c2.jpeg

性能測試

總結

  • 如果能夠只用 cornerRadius 解決問題,就不用優化。

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

  • UIImageView 的圓角通過直接截取圖片實現,其它視圖的圓角可以通過 Core Graphics 畫出圓角矩形實現。

  • 問啊-一鍵呼叫程式員答題神器,牛人一對一服務,開發者編程必備官方網站:www.wenaaa.com

    QQ群290551701 聚集很多互聯網精英,技術總監,架構師,項目經理!開源技術研究,歡迎業內人士,大牛及新手有志於從事IT行業人員進入!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.常用的方式是:將一張圖片設置為背景,然後在裡面加文字,你加入的圖片代碼是: <img src="img.jpg" width='100px" height="50px"> 改為 <div style="background:url('img.jpg') no-repeat;width:100px
  • 1.9.1以後的版本,好像不支持 jq 的 toggle function的用法啦。
  • JS代碼 //圖像載入出錯時的處理 function errorImg(img) { img.src = "預設圖片.jpg"; img.onerror = null; } HTML代碼 <img width="32" height="32" src="1.jpg" onerror="errorIm
  • 有關name: 一、頁面關鍵字 網站關鍵字:用戶通過搜索引擎能搜到該網站的辭彙。最好控制在10個以內。 基本語法: <meta name="keywords" content="具體的關鍵字"> 示例代碼: 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta ch
  • 時間就是金錢。編碼效率的提升意味著更多的收入。可是當我們的開發技巧已經到達一定高度時,如何讓開發效率更上一層樓呢?答案就是使用開發工具!在這篇文章中,我會向你介紹一些幫助我提升編碼速度和工作效率的工具。 Xcode插件 幾乎所有開發者都知道Alcatraz是一個開源的包管理工具,可以讓我們更輕鬆地管
  • 前言 iOS8之後系統自帶使用AVPlayerViewController播放視頻 AVPlayerViewController AVPlayerViewController和導航控制器差不多,需要將它添加在一個視圖控制器上,將它的視圖添加視圖上 1.導入頭文件 #import <AVKit/AVK
  • 在Swift 2.0版本中,Swift語言對其錯誤處理進行了新的設計,當然了,重新設計後的結果使得該錯誤處理系統用起來更爽。今天博客的主題就是系統的搞一下Swift中的錯誤處理,以及看一下Swift中是如何拋出異常的。在編譯型語言中,錯誤一般分為編譯錯誤和運行時錯誤。我們平時在代碼中處理的錯誤為運行
  • /** * 要求計算一個文件中所有文件的大小 註意:必須先計算一個文件夾中所有文件的大小必須先拿到所有文件,然後再獲取所有文件的大小,然後相加 */ NSFileManager *manager=[NSFileManager defaultManager]; //提取要求計算的文件 NSArray
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...