programming iOS - view drawing (一)

許多UIView的子類,如一個UIButton或一個UILabel,它們知道怎么繪制自己。遲早,你也將想要做一些自己的繪制。你可以事先準(zhǔn)備好您的繪圖作為一個圖像文件。您可以用代碼繪制一張圖片在應(yīng)用程序運行中。您可以在`UIView`子類,例如一個UIImageVie或一個UIButton中顯示在你的圖像。一個純粹的UIView有關(guān)于繪畫的一切,繪圖很大程度上取決于你;你的代碼決定view畫什么,你的界面就是什么。

Images and Image Views

基本的UIKit的圖像類是UIImageUIImage可以讀取磁盤上的文件,因此,如果圖像不需要動態(tài)創(chuàng)建,一種簡單的方式就是在應(yīng)用程序運行前提供一個圖片文件放到程序包資源中。系統(tǒng)知道如何處理標(biāo)準(zhǔn)圖像文件類型,如TIFF,JPEG,GIF和PNG;當(dāng)圖像文件將被包含在你的應(yīng)用程序包,你應(yīng)該優(yōu)先提供PNG格式的圖片,因為系統(tǒng)對PNG格式有種有特殊的親和力。還可以以其他方式獲取圖像數(shù)據(jù),例如通過下載,并轉(zhuǎn)變成一個UIImage。相反,你也可以用代碼構(gòu)造一個UIImage并顯示在你的界面或保存到磁盤。

Image Files

可以通過UIImage的初始化init(named:)方法來獲得app bundle中的圖片文件。這個方法在兩個地方查找圖片:

  • Asset catalog

    我們在asset catalog中查找與name名稱相同的圖片。該名稱是區(qū)分大小寫的。

  • Top level of app bundle

    我們在app bundle的最上層尋找與name名稱相同的圖片。該名稱是區(qū)分大小寫的,并應(yīng)包括文件擴展名;如果它不包括文件擴展名,則假定.PNG。

當(dāng)調(diào)用 init(named:)時,asset catalogapp bundle top level 之前搜索圖片文件。如果有多個asset catalog,它們都會進行搜索,但搜索順序是不確定的并且不能指定搜索順序,所以應(yīng)該避免圖像具有相同的名稱。

關(guān)于init(named:)的一個好處是圖像數(shù)據(jù)可能會緩存在內(nèi)存中,,如果你以后再通過調(diào)用init(named:)訪問為相同的圖像,緩存中的數(shù)據(jù)可以立即。或者,你可以使用init(contentsOfFile:)直接從 app bundle 里面讀取圖像數(shù)據(jù)而不緩存,但是需要提供一個路徑字符串作為參數(shù); 你可以通過NSBundle.mainBundle()獲取到app's bundle,然后通過NSBundle的實列方法,例如pathForResource:ofType獲取文件的路徑。

獲取app bundle內(nèi)資源的方法,如 init(named:)pathFor- Resource:ofType:,都會識別實際的資源文件的后綴。在double-resolution 屏幕上,當(dāng)通過文件名在 app bundle 內(nèi)獲取圖片時,具有相同圖片名并且含有@2x的圖片將自動使用,與由此生成的UIImagescale屬性值為2.0。同樣,如果文件具有相同的名稱并且有 @3x,它擴展將會用于iPhone 6 Plus,并且 scale 屬性值為 3.0

通過這種方式,您的應(yīng)用程序可以包含一個圖像文件在在不同分辨率下的多個版本。由于scale屬性,圖像的高分辨率版本和單分辨率的版本繪制的大小相同。因此,在高分辨率的屏幕上,代碼不用修改就能工作,但圖片看起來清晰。

同樣,具有相同名稱的由~ipad擴展的文件當(dāng)appipad上運行時會被自動使用 。你可以使用這種方式在universal app中根據(jù)不同的設(shè)備 iPhone 或者 iPad 提供不同的圖像(這對任何在bundle中的資源都是有效的而不只是圖片)

asset catalog的一個好處是你可以忘記所有關(guān)于這些后綴名約定。asset catalog知道在一個image set內(nèi)何時使用備用圖像,不是根據(jù)它的名字,而是根據(jù)它在catalog中的位置。把single-,double-,triple-resolution分辨率的圖片放到標(biāo)記著1x,2x,3x的位置上。對于不同的iPad版的圖像,檢查iPhoneiPad的圖像集的屬性檢查器,不同的圖片位置將會出現(xiàn)在asset catalog中。

Asset catalog也可以區(qū)分圖像在不同size class 下的版本。在圖像集的屬性檢查器中,使用寬度和高度的彈出菜單來指定要區(qū)分哪個size class。如果我們把運行著app的iPhone旋轉(zhuǎn)到橫向,如果有既有的圖片集中的Any heightCompact height圖像都可以使用的話,會優(yōu)先使用Compact height版本的圖像。這些功能是實時的在app運行期間;如果應(yīng)用程序從橫向旋轉(zhuǎn)為縱向,Any height會自動替換掉Compact height的圖片,如果圖片集中的2中圖像都可以使用的話。

Asset catalog這種神奇的能力是通過trait collectionsUIImageAsset類來實現(xiàn)的。當(dāng)圖像通過初始化init(named:)asset catalog中獲取時,它的imageAsset屬性是一個UIImageAsset。在圖像集所有的圖像都可以通過UIImageAsset獲取;每個圖像都有trait collection,你可以訪問圖像的imageAsset的屬性和 imageWithTraitCollection:從相同的圖像集中得到特定trait collection的圖片。

一個內(nèi)置的顯示圖像的interface object能自動的識別trait collection;它接收traitCollectionDidChange:消息并相應(yīng)地作出響應(yīng)。我們可以通過構(gòu)造一個有image屬性的UIView來實現(xiàn)這個功能:

class MyView: UIView {
    var image: UIView!
    override func traitCollectionDidChange(previous: UITraitCollection?) {
    self.setNeedDisplay()
    }
    override func drawRect() {
        if var im = self.image {
            if let asset = self.image.imageAsset {
                let tc = self.traitCollection
                im = asset.imageWithTraitCollection(tc)
            }
            im.drawAtPoint(CGPointZero)
        }
    }
}

此外,你的代碼也可以將圖像合并到一個UIImageAsset - 代碼相當(dāng)于一個asset catalog中的image set,但是并沒有asset catalog 。因此,你可以實時的創(chuàng)建圖像,或者在app bundle的外部獲取圖像,并且自動配置當(dāng)iPhoneportrait ori‐ entation時使用前一個圖像,當(dāng)iPhonelandscape orientation 時使用另一個圖像:

let tcdisp = UITraitCollection(displayScale:UIScreen.mainScreen().scale)
let tcphone = UITraitCollection(userInterfaceIdiom: .Phone)
let tcreg = UITraitCollection(verticalSizeClass: .Regular)
let tc1 = UITraitCollection(traitsFromCollections: [tcdisp, tcphone, tcreg]) 
let tccom = UITraitCollection(verticalSizeClass: .Compact)
let tc2 = UITraitCollection(traitsFromCollections: [tcdisp, tcphone, tccom]) 
let moods = UIImageAsset() 
let frowney = UIImage(named:"frowney")! 
let smiley = UIImage(named:"smiley")!
moods.registerImage(frowney, withTraitCollection: tc1)
moods.registerImage(smiley, withTraitCollection: tc2)

之后,如果把frowney放到用戶界面 - 例如,一個UIImageViewimage屬性 - ,當(dāng)app改變方向時,它將和smiley交替顯示。可喜的是,即使是沒有永久引用frowneysmiley,或UIImageAssetmoods),這都會自動發(fā)生。原因是,frowneysmiley由系統(tǒng)緩存(因為調(diào)用init(named:)),他們各自保持一個它們自己關(guān)聯(lián)的UIImageAsset的強引用。

通過init(named:inBundle:compatibleWith- TraitCollection:)app bundle或者asset catalog獲取圖像時可以指定一個目標(biāo)trait collectionbundle參數(shù)經(jīng)常為nil,這表示app‘s main bundle

Image Views

許多內(nèi)置的CocoaCocoa interface objects接受一個UIImage做為自己的一部分去繪制;例如,一個UIButton能夠顯示圖像,UINavigationBar或者UITabBar可以有一個背景圖像。當(dāng)你只是想一個圖像出現(xiàn)在你的界面上,你可能把它交給一個圖像視圖 - UIImageView - 它的作用就是顯示圖像。

nib編輯器在這方面提供了一些快捷方式:一個interface object的屬性檢查器中,會有一個彈出菜單,其中包含項目中的所有圖像,這些圖像也會在Media library中顯示(Command-Option- Control-4)。Media library的圖像往往可以拖動到畫布上的interface object上顯示,如果只是拖動Media library里的圖像到空白的view,它被轉(zhuǎn)換成顯示該圖像的一個UIImageView

一個UIImageView實際上可以有兩幅圖片,其中一個分配給自己的image 屬性和另一個分配給其highlightedImage屬性;UIImageViewhighlighted屬性值決定在任何給定的時間顯示哪幅圖。和button一樣,UIImageView只會在用戶點擊它的時候高亮顯示。不過,在某些特定的情況下UIImageView會對它周圍的高亮作出回應(yīng);例如在table view cell中,當(dāng)cell高亮的時候,ImageView會顯示它的highlighted image

UIImageView也是UIView,因此它可以有圖像屬性外也可以有背景顏色,它可以有一個alpha(透明度)值,等等。圖像可能有透明的區(qū)域,也可以是任何形狀,UIImageView都會顯示它;沒有背景顏色的UIImageView不會顯示在界面中,除非它的image屬性不為空,圖像僅僅只是顯示在界面中,用戶不會意識到它是在一個矩形框中。即沒有背景顏色,image屬性也沒有值的UIImageView是不可見的,所以你可以以一個空的UIImageView開始,并隨后在代碼中指定image屬性。你可以指定一個新的圖像來代替舊的,或者設(shè)置image屬性的值為nil來移除UIImageView的圖像。

UIImageView如何繪制自己的圖像取決于其contentMode屬性(UIViewContentMode)的設(shè)置。 (該contentMode屬性是從UIView繼承的)例如,.ScaleToFill意味著圖像的寬度和高度都設(shè)置為視圖的寬度和高度,從而完全填充視圖即使這會改變圖像的長寬比;.Center居中繪制圖像而且不改變t圖像的大小。理解contentMode最好的方式是在nib中為UIImageView分配一個小圖像,然后在屬性檢查器中,切換不同的mode,觀察圖像如何繪制自身。

你還應(yīng)該注意UIImageViewclipsToBounds屬性;如果是false,即使它的圖像比image view更大,或者圖像沒有被contentMode按比例縮小,那么圖像可以延伸超出image view本身全部顯示。

當(dāng)在代碼中創(chuàng)建UIImageView,你可以充分利用便利構(gòu)造器的優(yōu)勢,init(image:) (或者 init(image:highlightedImage:)).默認(rèn)的contentMode.ScaleToFill,但圖像初始時是不縮放的,而是調(diào)整view自身的大小去匹配圖像。你仍然可能需要把UIImageView放到它的父視圖上正確位置。在下面這個例子中,我把火星圖片在應(yīng)用程序界面中心:

let iv = UIImageView(image: UIImage(named: "Mars"))  //asset catalog
mainView.addSubview(iv)
iv.center = iv.superview!.bounds.center
iv.frame.makeIntegralInPlace()

為一個已經(jīng)存在的UIImageView指定圖片時,UIImageView的大小如何改變?nèi)Q于它是否使用autolayout。如果沒有使用autolayout,或者它的大小被約束完全限定,那么UIImageView的大小不發(fā)生變化。但在autolayout下,除非其他約束阻止,新的圖像的尺寸會變?yōu)?code>image view的intrinsicContentSize,因此imageview將變?yōu)閳D像的大小。

image view會自動從圖像的alignmentRectInsets獲得其alignmentRectInsets。因此,如果你打算使用自動布局來調(diào)整image view對齊到其他對象,你可以將相應(yīng)的alignmentRectInsets設(shè)置到將要顯示的圖像上,那么image view會正確的顯示。要實現(xiàn)這個功能,通過在原始圖像上調(diào)用imageWithAlignmentRectInsets派生出新的圖像。

從理論上講,你應(yīng)該能夠在Asset catalog中設(shè)置圖像的alignmentRectInsets。但是當(dāng)寫這篇文章時,此功能無法正常工作。

Resizable Images

在界面中某些地方可能需要可以調(diào)整大小的圖像;例如,在作為一個滑塊或進度條視圖的軌道的自定義圖像必須能夠調(diào)整大小,以便它可以填充任何長度的空間。還有經(jīng)常需要通過平鋪或拉伸現(xiàn)有圖像填充背景的其他情形。

通過在正常的圖像上調(diào)用resizableImageWithCapInsets:resizingMode:方法來創(chuàng)建動態(tài)伸縮的圖像。capInsets:參數(shù)是一個UIEdgeInsets,其分量代表向內(nèi)到圖像的邊緣的距離(可以理解為內(nèi)邊距)。在一個比圖像大的context中,可調(diào)整大小的圖像有2種表現(xiàn)方式,這取決于resizingMode:的值(UIImageResizingMode):

  • .Tile


    變化的區(qū)域的內(nèi)部圖片是平鋪(重復(fù));每一條邊是由非變化區(qū)域的相應(yīng)邊緣矩形組成的。相對于變化區(qū)域的四個角落的繪制不變。

  • .Stretch


    變化區(qū)域的內(nèi)部被拉伸一次以填充;每一條邊是由非變化區(qū)域的相應(yīng)邊緣矩形組成的。相對于變化區(qū)域的四個角落的繪制不變。

在下面的例子中,假設(shè)self.iv是一個絕對高度和寬度(因此它不會用它的圖像的大小來設(shè)置自己的大小)而且contentMode屬性為.ScaleToFill(圖像會有伸縮的行為)的UIImageView。首先,我會說明怎么平鋪整個圖像;注意,capInsets:的值是UIEdgeInsetsZero

let mars = UIImage(named: "Mars")!
let marsTiled = mars.resizableImageWithCapInsets(UIEdgeInsetsZero, resizingMode: .Tile)
self.iv.image = marsTiled

然后,改變上面代碼種capInsets參數(shù)的值來重新平鋪上面的圖片:

let marsTiled = mars.resizableImageWithCapInsets(
    UIEdgeInsetsMake(
        mars.size.height / 4.0, 
        mars.size.width / 4.0, 
        mars.size.height / 4.0, 
        mars.size.width / 4.0
    ), resizingMode: .Tile)

然后來說明拉伸,從改變上面代碼的resizingMode開始:

let marsTiled = mars.resizableImageWithCapInsets(
    UIEdgeInsetsMake(
        mars.size.height / 4.0, 
        mars.size.width / 4.0, 
        mars.size.height / 4.0, 
        mars.size.width / 4.0
    ), resizingMode: .Stretch)

效果如下:


一個常見的延伸策略是讓幾乎一半的原始圖像作為cap inset,只留下中心的一兩個像素來填充空白區(qū)域:

let marsTiled = mars.resizableImageWithCapInsets(
    UIEdgeInsetsMake(
        mars.size.height / 2.0 - 1, 
        mars.size.width / 2.0 - 1, 
        mars.size.height / 2.0 -1, 
        mars.size.width / 2.0 - 1
    ), resizingMode: .Stretch)

效果如下


你也應(yīng)該嘗試不同的contentMode設(shè)置。在上的例子中,如果該圖像視圖的contentMode.ScaleAspectFill,并且如果圖像視圖的clipsToBoundstrue,我們會得到一種漸變的效果,因為拉神完成的圖象的頂部和底部已經(jīng)超出image view的邊界而不會被繪制出來。
效果如下:

你可以通過項目的asset catalog而不是代碼來配置一個可調(diào)整大小的圖像。經(jīng)常出現(xiàn)的情況是:一個特定的圖像將在您的應(yīng)用中主要被用來作為一個可調(diào)整大小的圖像,并且總是具有同樣的capInsetsresizingMode,所以很有必要只配置此圖像一次,而不是重復(fù)寫相同的代碼。即使圖像在asset catalog中配置為可調(diào)整大小,它也可以做為一個正常的圖片出現(xiàn)在你的界面中, 例如,你可以把它分配給根據(jù)圖像大小調(diào)整自身大小的image view,或者是不會壓縮或者拉伸自己圖像的image view

要在asset catalog中配置一個可調(diào)整大小的圖像,首先選擇圖像,然后在Slicing section的屬性檢查器中,更改Slices彈出菜單為Horizontal,Vertical,或者是Horizontal and Vertical。當(dāng)你執(zhí)行此操作時,會有更多的界面出現(xiàn)供你配置參數(shù)。您可以在另外的彈出菜單中配置resizingMode。也可以用數(shù)字,或單擊畫布右下角的Show Slicing,這都會在畫布上顯示配置好的圖像。圖形編輯器是可縮放的,所以你可以放大到你覺得舒服的大小。

這個功能實際上比resizableImageWithCapInsetsresizingMode:更加強大.它可以讓你從平鋪或拉伸區(qū)域分別指定結(jié)束的caps,而剩下的部分將會被切掉。如下圖所示:


在上圖中左上角,右上角,左下角和右下角的暗色區(qū)域?qū)⒃瓨永L制。窄帶將被拉長,頂部中心的小矩形將被拉伸以填充大部分的區(qū)域。但是圖像的其余部分,被紗布幕覆蓋的中央大片區(qū)域,將被完全省略。結(jié)果如下圖:

Image Rendering Mode

iOS應(yīng)用的用戶界面在某些地方會自動將圖像作為一個透明遮罩(transparency mask),也被稱為template。這意味著圖像的每個像素的透明度(alpha)會起作用,而顏色值將被忽略。在屏幕上顯示的圖像是由圖像的透明度值和一個純色(tint color)混合而成。tab bar item的圖片顯示就是這種方式。

圖像被怎樣顯示是通過圖像的只讀renderingMode的屬性控制的;只能通過在圖像上調(diào)用imageWithRenderingMode:來生成一個新的圖像來更改rendering mode值.渲染模式的值(UIImageRenderingMode)為:

  • .Automatic
  • .AlwaysOriginal
  • .AlwaysTemplate

默認(rèn)值是.Automatic。這意味在某些被限制的上下文中它被用作透明遮罩(transparency mask)外,大部分情況都會被原樣繪制出來。

通過設(shè)置renderingMod屬性,你可以強制正常繪制圖像,即使在一個把它當(dāng)作一個透明遮罩(transparency mask)的上下文中。相反的:你可以強制繪制圖像為透明度遮罩(transparency mask),即使在正常的上下文中。

為了實現(xiàn)這個功能,iOS給每個UIView一個tintColor屬性,這將對它包含的任何模板圖像進行著色。此外,tintColor默認(rèn)是從應(yīng)用程序的最上面window一直貫穿整個視圖層級而繼承的。因此,更改應(yīng)用程序的主windowtintColor可能是你僅有的能對window所做的改變之一;否則,你的應(yīng)用程序?qū)⒉捎孟到y(tǒng)的藍(lán)色的tint color。 (如果你使用的是main storyboard,可以在文件檢查器中設(shè)置Global Tint color的顏色。)可以設(shè)置個別viewtint color, 這會被他的子視圖繼承。下圖顯示了一個應(yīng)用中主windowtintcolor是紅色的,顯示相同的背景圖片的兩個按鈕,一個正常渲染,另一個以模板方式渲染:

asset catalog中可以設(shè)置圖像的渲染模式。在asset catalog中選擇圖片,在屬性檢查器中使用彈出的Render菜單設(shè)置渲染模式為默認(rèn)值(.Automatic),原始圖像(.AlwaysOriginal),或模版圖像(.AlwaysTemplate) 。這是一個很好的辦法,這使你每次使用相同的渲染模式設(shè)置圖片時少寫很多代碼。而且,當(dāng)調(diào)用init(named:),會返回已經(jīng)設(shè)置好渲染模式的圖片。

Reversible Images

新的iOS9系統(tǒng)中,當(dāng)你的app是在一個本地語言是從右到左的系統(tǒng)上運行的時候,整個用戶界面會自動改變翻轉(zhuǎn)(改變方向)。一般情況下,這不會影響到你的圖片。runtime會假定你不想反轉(zhuǎn)圖片當(dāng)用戶界面反轉(zhuǎn)的時候,因此其默認(rèn)行為是讓他們保持原樣。

不過,你可能希望圖片隨著系統(tǒng)一起反轉(zhuǎn)。例如,可能你已經(jīng)繪制好一個當(dāng)用戶點擊按鈕后指向新界面出現(xiàn)的方向的箭頭。如果按鈕作用是在導(dǎo)航界面進入一個新的視圖控制器,在從左往右的系統(tǒng)圖片箭頭方向指向右邊,但是在從右往左的系統(tǒng)上圖片箭頭方向應(yīng)該指向左邊。此圖像在應(yīng)用程序的界面上具有方向的信息;因此它需要水平翻轉(zhuǎn)當(dāng)界面反轉(zhuǎn)時。

通過調(diào)用圖像的imageFlippedForRightToLeftLayoutDirection,并在界面使用新生成的圖像來達到上面的效果。在左到右系統(tǒng)中,正常的圖像將被使用;而在右到左的系統(tǒng)上時,將會自動創(chuàng)建并使用對應(yīng)的相反的圖片。您可以重寫此行為,即使圖像是可反轉(zhuǎn)的。對于UIView,例如UIImageView,通過設(shè)置viewsemanticContentAttribute防止圖片鏡像。

不幸的是,在asset catalog中還沒有辦法指定圖片是可反轉(zhuǎn)的。因此,在界面中出現(xiàn)的圖片 - 例如,在nib中配置的UIImageView - 你必須使用代碼去控制。在下面這個例子中,我在視圖控制器的viewDidLoad方法中取出圖像視圖(self.iv)的圖片,并用它自身的可反轉(zhuǎn)版本替換它自己:

override func viewDidLoad() {
    super.viewDidLoad()
    self.iv.image = self.iv.image?.imageFlippedForRightToLeftLayoutDirection()
}

Graphics Contexts

你可能想使用代碼繪制一些圖像,而不是僅僅使用已經(jīng)存在的圖片文件。要實現(xiàn)這一點,你需要一個圖形上下文(graphics context)。

圖形上下文就是你可以繪制圖像的地方。反之,你不能用代碼繪制,除非你有一個圖形上下文。有幾種方法可以得到一個圖形上下文;我將主要介紹兩種已經(jīng)被我的經(jīng)驗證明是最常見的方式:

  • You create an image context
    UIGraphicsBeginImageContextWithOptions函數(shù)會創(chuàng)建一個適合用于繪制圖像的上下文。你可以繪制到此上下文并生成圖像。當(dāng)這樣做之后,調(diào)用UIGraphicsGetImageFromCurrentImageContext把上下文變成一個UIImage,然后調(diào)用UIGraphicsEndImageContext清除上下文。現(xiàn)在你有了一個圖片,你可以讓它顯示到界面上或者繪制到一些其他的圖形上下文或者保存為一個文件。
  • Cocoa hands you a graphics context
    你子類化UIView然后實現(xiàn)drawRect:方法.當(dāng)你的drawRect:方法被調(diào)用的時候,Cocoa同時會為你創(chuàng)建一個圖形上下文,并要求你在里面繪制;不管你畫什么UIView都會顯示出來。
    這種情況有一種輕微的變體是你繼承CALayer并在layerdelegate里面實現(xiàn)drawInContext:,或?qū)崿F(xiàn)drawLayer:inContext

此外,在任何時刻圖形上下文要么是或不是當(dāng)前的圖形上下文:

  • UIGraphicsBeginImageContextWithOptions不僅創(chuàng)造了一個圖形上下文,這也使得上下文成為當(dāng)前的圖形上下文。
  • 當(dāng)drawRect:被調(diào)用時,UIView的圖形上下文已經(jīng)是當(dāng)前的圖形上下文。
  • 有一個context:參數(shù)的回調(diào)函數(shù)并不會使任何上下文變成當(dāng)前的圖形上下文;相反,該參數(shù)是一個想要你在里面繪制的圖形上下文的引用,但是只有它稱為當(dāng)前的圖形上下文你才能在它里面繪制,是否這么做取決于你。

初學(xué)者對于繪制感到困惑的原因是有兩套關(guān)于繪制圖像的工具,但是它們對于繪制圖像時的context的處理方式一點都不相同。一組需要一個當(dāng)前上下文;另一組只是需要一個上下文:

  • UIKit
    很多Cocoa類知道如何繪制它們自己;其中包括UIImageNSString(繪制文本),UIBezierPath(用于繪制形狀),和UIColor。有些類提供能力有限但是方便的方法;有些類則非常強大。在許多情況下,UIKit會滿足你所以的需求。
    你只能繪制到當(dāng)前圖形上下文中當(dāng)你使用UIKit時。所以,如果你在一個UIGraphicsBeginImageContextWithOptionsdrawRect:情況下,你可以直接使用UIKit便利的方法;而且會有一個當(dāng)前的圖形上下文,它就是你想在里面繪制的上下文。另外,如果你有一個context:的參數(shù),而且你還想使用UIKit便利的方法,你只能把上下文變成當(dāng)前的圖形上下文;通過調(diào)用UIGraphicsPushContext做到這一點(并確保用UIGraphicsPopContext來還原)。
  • Core Graphics
    這是一個完整的繪圖API。Core Graphics,通常被稱為Quartz ,或Quartz 2D,是iOS繪圖系統(tǒng)的基礎(chǔ) - UIKit的繪制是建立在它之上 - 所以它是低層次的,包含很多C函數(shù)。
    使用Core Graphics繪制時,你必須在每個函數(shù)調(diào)用中明確的指定將要繪制的圖形上下文。如果你你有一個context:參數(shù),這可能是你想要繪制的圖形上下文。在UIKit中你不需要一個context的引用,但是在Core Graphic中你需要一個context的引用。既然你在當(dāng)前的圖形上下文里面繪制,通過調(diào)用UIGraphicsGetCurrentContext來得到當(dāng)前圖形上下文的引用。

你不必分開使用UIKitCore graphics。相反,你可以在同一個代碼塊中使用UIKit和Core graphics操作相同的圖形上下文。他們只是控制圖形上下文繪制的兩種不同方式。

因此,在一個圖形上下文中我們有兩套工具和三種方法,總共六種方式來繪圖。在下面的例子中,我將說明這六種方法。首先,我將通過繼承UIView然后實現(xiàn)drawRect:方法,在UIKit為我們準(zhǔn)備好的當(dāng)前圖形上下文中畫一個藍(lán)色圓圈:

override func drawRect(rect: GGRect) {
    let p = UIBezierPath(ovalInRect: CGRectMake(0, 0, 100, 100))
    UIColor.blueColor().setFill()
    p.fill()
}

下面我將會用Core graphics實現(xiàn)相同的功能,但是得先獲得當(dāng)前圖形上下文得引用:

override func drawRect(rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()
    CGContextAddEllipseInRect(context, CGRectMake(0, 0, 100, 100))
    CGContextSetFillColorWithColor(context, UIColor.blueColor().CGColor)
    CGContextFillPath(context)
}

接下來我會實現(xiàn)一個UIView子類的drawLayer:inContext:方法。在這個例子中,會有一個context的引用,但是它并不是當(dāng)前的圖形上下文,所以為了使用UIKit,我得先讓它成為當(dāng)前得圖形上下文:

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    UIGraphicsPushContext(context)
    let p = UIBezierPath(ovalInRect: CGRectMake(0, 0, 100, 100))
    UIColor.blueColor().setFill()
    p.fill()
    UIGraphicsPopContext()
}

然后我引用以下傳進來得context就可以在drawLayer:inContext中使用Core graphics:

override func drawLayer(layer, inContext context: CGContext) {
    CGContextAddEllipseInRect(context, CGRectMake(0, 0, 100, 100))
    CGContextSetFillColorWithColor(context, UIColor.blueColor().CGColor)
    CGContextFillPath(context)
}

最后,我將做一個藍(lán)色圓圈得圖片。我們可以在任何時候(我們不需要等待某些特定方法被調(diào)用),并在任何類(我們不需要繼承UIView)中實現(xiàn)此功能。由此產(chǎn)生的UIImage適用于可以使用UIImage的任何地方。例如,你可以把它賦值給UIImageViewimage屬性,從而導(dǎo)致圖像出現(xiàn)在屏幕上。或者你可以將其保存為一個文件。
首先我會使用UIKit來繪制我的圖片:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), false, 0)
let p = UIBezierPath(ovalInRect: CGRectMake(0, 0, 100, 100))
UIColor.blueColor().setFill()
p.fill()
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

下面是使用Core Graphics實現(xiàn)相同的功能:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), false, 0)
let context = UIGraphicsGetCurrentContext()!
CGContextAddEllipseInRect(context, CGRectMake(0, 0, 100, 100))
CGContextSetFillColorWithColor(context, UIColor.blueColor().CGColor)
CGContextFillPath(context)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

你可能想知道UIGraphicsBeginImageContextWithOptions各項參數(shù)的意義。第一個參數(shù)很明顯是要創(chuàng)建圖片的尺寸。第二個參數(shù)聲明圖像是不是應(yīng)該透明;如果這個參數(shù)為true而不是false,繪制的圖片將有一個黑色的背景,這并不是我想要的效果。第三個參數(shù)指定圖像的縮放系數(shù);通過傳遞0,告訴系統(tǒng)根據(jù)主屏幕來設(shè)置圖片的縮放系數(shù),所以我的圖片在單分辨率和高分辨率的設(shè)備上都會顯示很好。

UIImage Drawing

UIImage提供繪制自身到當(dāng)前的圖形上下文的方法。我們知道如何獲得UIImage,我們知道如何獲得圖形上下文并使其成為當(dāng)前的圖形上下文,所以我們可以嘗試使用這些方法來繪制圖片。

下面,我會做出一張由兩張火星圖片并排組成的新圖片:

let mars = UIImage(named: "Mars")!
let size = mars.size
UIGraphicsBeginImageContextWithOptions(CGSizeMake(size.width * 2, size.height * 2), false, 0.0)
mars.drawAtPoint(CGPointMake(0, 0))
mars.drawAtPoint(CGPointMake(size.width, 0))
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

效果如下:


在上面的例子中圖片的縮放控制的很好。如果有原始的火星圖像的多個分辨率版本,系統(tǒng)會根據(jù)當(dāng)前設(shè)備選擇正確的圖片,并指定正確的scale值。調(diào)用UIGraphicsBeginImageContextWithOptions函數(shù)的第三個參數(shù)為0,因此在圖像上下文中繪制的圖片也會有正確的scale值。而且從UIGraphicsGetImageFromCurrentImageContext得到的圖像也會有正確的scale值。因此,上面的代碼生成的圖片在當(dāng)前設(shè)備上看起來顯示效果很好,無論它的屏幕分辨率是多少。

UIImage的其他一些方法,可以讓你在需要繪制的矩形中指定縮放系數(shù),并指定和矩形中已經(jīng)存在的東西的混合模式。為了說明這一點,我將創(chuàng)建一個兩倍大的火星圖片顯示在另一個火星圖片的中心,使用.Multiply混合模式:

let mars = UIImage(named: "Mars")!
let size = mras.size
UIGraphicsBeginImageContextWithOptions(CGSizeMake(size.width * 2, size.height * 2), false, 0)
mars.drawInRect(CGRectMake(0, 0, size.width * 2, size.height * 2))
mars.drawInRect(CGRectMake(sizw.width / 2, size.height / 2, size.width, size.height),
    blendMode: .Multiply, 
    alpha: 1.0)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

效果如下圖:


UIImage的繪制方法沒有辦法指定原始矩形-你要提取原始圖像的某已小塊區(qū)域。您可以通過創(chuàng)建一個較小的圖形上下文然后把圖片的指定區(qū)域繪制到里面。例如,為了獲得火星的右半部分的圖像,你可以創(chuàng)建一個只有火星圖像寬度一半的圖形上下文,然后左移繪制火星,這樣火星圖片只有右半部分在圖形上下文中。這樣做沒有什么壞處,這個一種非常標(biāo)準(zhǔn)的策略來獲取原始圖片的某部分區(qū)域。代碼如下:

let mars = UIImage(named: "Mars")!
let size = mars.size
UIGraphicsBeginImageContextWithOptions(CGSizeMake(size.width / 2.0, size.height), false, 0.0)
mars.drawAtPoint(CGPointMake(-size.width / 2.0, 0))
let im = UIGraphicsGetImageFromCurrenImageContext()
UIGraphicsEndImageContext()

效果如下圖:


CGImage Drawing

UIImageCore graphics的版本是CGImage。他們很容易相互轉(zhuǎn)換:UIImage有一個訪問其Quartz圖像數(shù)據(jù)的CGImage屬性,你可以在CGImage上調(diào)用init(CGImage:)或更容易配置的init(CGImage:scale:orientation:)方法來得到一個UIImage。

CGImage允許你直接從原始的UIImage的某個矩形區(qū)域創(chuàng)建一個新的圖片,而UIImage不能這么做。(CGImage還有其他UIImage不具備的能力;例如,您可以為CGImage應(yīng)用一個圖片遮罩(image mask)。)我會將火星圖片分為兩半然后分開繪制來說明:

let mars = UIImage(named: "Mars")!
let marsCG = mars.CGImage
let size = mars.size
let marsLeft = CGImageCreateWithImageInRect(marsCG, CGRectMake(0, 0, size.width / 2.0, size.height))
let marsRight = CGImageCreateWithImageInRect(marsCG, CGRectMake(size.width / 2.0, 0, size.width / 2.0, size.height))
UIGraphicsBeginImageContextWithOptions(CGSizeMake(size.width * 1.5, size.height * 1.5), false, 0)
let context = UIGraphicsGetCurrentContext()!
CGContextDrawImage(context, CGRectMake(0, 0, size.width / 2, size.height), marsLeft)
CGContextDrawImage(context, CGRectMake(sizw.width, 0, size.width / 2.0, size.height), marsRight)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

效果如下:


但是,上面例子有個問題:繪圖是上下顛倒的!代碼沒有對它執(zhí)行翻轉(zhuǎn);但是它是上下鏡像的,或使用專業(yè)的技術(shù)術(shù)語:flipped。當(dāng)你創(chuàng)建一個CGImage,然后用CGContextDrawImage繪制它的時候就會出現(xiàn)這種情況,這是由于原始的上下文和目標(biāo)上下文的坐標(biāo)系統(tǒng)不匹配造成的。

有很多種方式來修復(fù)不同坐標(biāo)系統(tǒng)之間的不匹配。其中一種是繪制CGImage到一個中間的UIImage然后從中間UIImage中提取出另一個CGImage。下面的例子通過一個函數(shù)來實現(xiàn)這個功能:

func flip(im: CGImage) -> CGImage {
    let size = CGSizeMake(
        CGFloat(CGImageGetWidth(im),
        CGFloat(CGImageGetHeight(im))))
    UIGraphicsBeginImageContextWithOptions(size, false, 0)
    let context = UIGraphicsGetCurrentContext()!
    CGContextDrawImage(context, 
        CGRectMake(0, 0, size.width, size.height), 
        im)
    let result = UIGraphicsGetImageFromCurrentImageContext().CGImage
    UIGraphicsEndImageContext()
    return result!
}

使用上面那個便利的函數(shù),我們可以修復(fù)上面例子中使用CGContextDrawImage繪制兩半火星的正確方法了:

CGContextDrawImage(context, 
    CGRectMake(0, 0, size.width / 2.0, size.height),
    flip(marsLeft!))
CGContextDrawImage(context, 
    CGRectMake(size.width, 0, size.width / 2.0, sizw.height),
    flip(marsRight!))

但是,我們還有一個問題:一個高分辨率的設(shè)備上,如果我們的圖片有一個高分辨率的版本,那么繪制就會出錯。原因是我們使用UIImageinit(named:)方法獲取的火星圖片,在高分辨率的設(shè)備上它返回一個更大尺寸的火星圖像,通過設(shè)置圖片的scale屬性來匹配原始火星圖片的大小。但CGImage并沒有scale屬性,而且也不知道圖片的尺寸增加了!因此,在一個高分辨率的設(shè)備上,我們從火星圖片中提取的mars.CGImagemars.size的尺寸要大,并且之后所有的計算都是錯誤的。

處理CGImage最好的解決辦法是,把它封裝為一個UIImage并繪制這個UIImage而不是直接繪制CGImage。從CGImage生成UIImage時可以調(diào)用init(CGImage:scale:orientation:)來避免圖片的縮放。而且直接繪制UIImage而不是CGImage也避免翻轉(zhuǎn)問題!下面是不使用flip函數(shù)來處理翻轉(zhuǎn)和縮放的方法:

let mars = UIImage(named: "Mars")!
let size = mars.size
let marsCG = mars.CGImage
let sizeCG = CGSizeMake(
    CGFloat(CGImageGetWidth(marsCG),
    CGFloat(CGImageGetHeight(marsCG))))
let marsLeft = CGImageCreateWithImageInRect(
    marsCG,
    CGRectMake(0, 0, sizeCG.width / 2, sizeCG.height))
let marsRight = CGImageCreateWithImageInRect(
    marsCG,
    CGRectMake(sizeCG.width / 2.0, 0, sizeCG.width / 2.0, sizeCG.height))
UIGraphicsBeginImageContextWithOptions(
    CGSizeMake(size.width * 1.5, size.height), 
    false,
    0.0)
//instead of calling flip, pass through UIImage
UIImage(CGImage: marsLeft!, 
    scale: mars.scale,
    orientation: mars.imageOrientation)
    .drawAtPoint(CGPointMake(0, 0))
UIImage(CGImage: marsRight!,
    scale: mars.scale,
    orientation: mars.imageOrientation)
    .drawAtPoint(CGPointMake(sizw.width, 0))
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
翻轉(zhuǎn)發(fā)生的原因
意外翻轉(zhuǎn)發(fā)生的原因是Core graphics移植于OS XOS X的坐標(biāo)系的原點默認(rèn)情況下位于左下角和y軸增長方向是向上的,而iOS上的原點默認(rèn)情況下在位于左上角并且y軸正方向是向下的。在大多數(shù)繪圖的情況下,因為圖形上下文的坐標(biāo)系統(tǒng)被自動調(diào)整,所以這不會有什么問題。因此在iOS中使用Core Graphics繪制時上下文中的坐標(biāo)系統(tǒng)的原點在左上角,這和你期望的是一樣的。但是,創(chuàng)建和繪制CGImage暴露了兩個坐標(biāo)世界之間的不兼容

另一個解決方案是,繪制CGImage之前對圖形上下文做一個變換(transform),直接翻轉(zhuǎn)上下文的內(nèi)部坐標(biāo)系統(tǒng)。這是非常有效的,但是如果已經(jīng)存在其他變換,這會非常混亂。

Snapshots

整個視圖 -- 從任何一個單一的按鈕到你整個界面包含視圖整個層次結(jié)構(gòu)--可通過調(diào)用UIView的實例方法drawViewHierarchyInRect:afterScreenUpdates:可以被繪制到當(dāng)前的圖形上下文中. (此方法比CALayerrenderInContext:速度快很多;不過,renderInContext:確實很方便)。繪制的結(jié)果是原始視圖的快照(snapshot):它看起來像原來的視圖,但它本質(zhì)上只是原來視圖的一個位圖圖像。

獲得視圖的快照更快的方法是使用的UIView(或UIScreen)的實例方法snapshotViewAfterScreenUpdates:.結(jié)果是一個UIView,而不是一個UIImage;這很像一個只知道如何繪制一個圖像--即快照--的UIImageView。這樣的快照視圖通常會被用作擴大其邊界和拉伸其圖像。如果你想拉伸快照讓它表現(xiàn)得像一個可調(diào)整大小的圖像,調(diào)用resizableSnapshotViewFromRect:afterScreenUpdates:withCapInsets:

Snapshots非常有用因為IOS界面的動態(tài)性質(zhì)。例如,你可以在真正的視圖上面放置一個快照從而隱藏真正的視圖上正在發(fā)生的事情,或者在動畫中移動快照而不是真正的視圖。

下面是我的應(yīng)用程序中的一個例子。這是一個紙牌游戲,它的視圖都是紙牌。我想用動畫把所有的紙牌從屏幕里面移動到屏幕外面。但我不想移動那些視圖本身!他們需要留在原地,繪制以后出現(xiàn)的紙牌。所以我為每個紙牌視圖做了一個快照視圖;然后,我隱藏真正的紙牌視圖,而把快照視圖放在它們的位置上,然后用動畫移除這些快照。代碼如下:

for v in views {
    let snapshot = v.snapshotViewAfterScreenUpdates(false)
    let snap = MySnapBehavior(item: snapshot, 
        snapToPoint: CGPointMake(
            self.anim.referenceView!.bounds.midX,
            -self.anim.referenceView!.bounds.height))
    self.snaps.append(snapshot)
    snapshot.frame = v.frame
    v.hidden = ture
    self.anim.referenceView!.addSubview(snapshot)
    self.anim.addBehavior(snap)
}

CIFilter and CIImage

CIFilterCIImage中的CI代表Core Image,通過數(shù)學(xué)變換轉(zhuǎn)化圖像的技術(shù)。Core Image首先在桌面(OS X)環(huán)境中使用,并且當(dāng)它被遷移到iOS系統(tǒng)時,一些在桌面上可用的濾鏡無法在IOS上使用(大概是因為對于移動設(shè)備來說這么數(shù)學(xué)運算太過于復(fù)雜)。多年來越來越多的OS X濾鏡被添加到iOS上,而現(xiàn)在在新的iOS9系統(tǒng)中,兩者沒有差別:所有OS X濾鏡在iOS中都是可用的,并且兩個平臺都具有幾乎相同的API 。

一個CIFilter就是一個濾鏡。可用的濾鏡可以分為幾大類:

  • Patterns and gradients
    這些濾鏡創(chuàng)建的CIImage可以與其他的CIImage,諸如單一的顏色,顏色盤,條紋,或梯度組合。
  • Compositing
    這些濾鏡使用圖像處理軟件如Photoshop的混合模式來組合圖片。
  • Color
    這些濾鏡調(diào)整或改變圖像的顏色。因此,你可以改變圖像的飽和度,色調(diào),亮度,對比度,伽瑪值和白平衡,曝光,陰影和高光,等等。
  • Geometric
    這些濾鏡對圖片執(zhí)行基本的幾何變換,如縮放,旋轉(zhuǎn)和裁剪。
  • Transformation
    這些濾鏡對圖片進行變形,模糊,或風(fēng)格化。
  • Transition
    這些濾鏡提供了一個圖像和另一個之間轉(zhuǎn)換的幀;通過順序請求幀,可以設(shè)置過渡動畫效果。
  • Special purpose
    這些濾鏡執(zhí)行高度專業(yè)化的操作,如人臉檢測和生成的QR碼。

CIFilter的基本用法相當(dāng)簡單︰

  • 您可以通過提供濾鏡的字符串名稱來指定想使用的濾鏡;想知道都有哪些名字,查閱Core Image Filter Reference,或調(diào)用CIFilter的類方法filterNamesInCategories:并提供一個nil參數(shù)。
  • 每個濾鏡具有少量的鍵值對確定其行為。您可以通過代碼了解全部的鍵,但通常你會參考文檔。對于你感興趣的每個鍵,你提供的一個鍵 - 值對。在提供值中,數(shù)字必須被包裝成NSNumber,并且其他有用的類,如CIVectorCIColor,其使用很容易理解。

CIFilter鍵中輸入的圖像或被濾鏡操作的圖片必須是CIImage。可以從CGImage中獲取CIImage通過調(diào)用init(CGImage:)或者從UIImage中獲取CIImage通過調(diào)用init(image:)

不要嘗試直接從UIImageCIImage屬性獲取CIImage。此屬性不會把UIImage轉(zhuǎn)換成CIImage!它僅僅只是指向支持(back)UIImageCIImage,如果這個UIImage真的是被CIImage支持(back)的;但是有可能你的圖片不是由CIImage支持的,而是由一個CGImage

然而你可以從濾鏡的輸出中獲取CIImage--這意味著濾鏡可以鏈?zhǔn)秸{(diào)用。
有三種方式來描述和使用濾鏡:

  • 通過CIFilterinit(name:)創(chuàng)建濾鏡。通過重復(fù)調(diào)用setValue:forKey:方法或者通過調(diào)用setValuesForKeysWith- Dictionary: 來為濾鏡添加鍵值對。通過濾鏡的outputImage獲取CIImage
  • 通過調(diào)用CIFilterinit(name:withInputParameters:)并提供鍵值對創(chuàng)建濾鏡。通過濾鏡的outputImage獲取CIImage
  • 如果CIFilter需要輸入圖像而且你已經(jīng)有了一個CIImage,可以通過調(diào)用CIImage實例方法imageByApplyingFilter:withInputParameters:并提供濾鏡和鍵值對,然后獲取輸出的CIImage

當(dāng)你構(gòu)建一個濾鏡鏈時,實際上什么都沒有發(fā)生。唯一的密集計算會發(fā)生在最后,當(dāng)你變換濾鏡鏈輸出的CIImage為位圖圖形時。這就是所謂的圖像渲染。有兩種主要方式的方式實現(xiàn)這一點:

  • With a CIContext
    通過調(diào)用init(options:)創(chuàng)建一個CIContext。然后把最后的CIImage作為第一個參數(shù)傳遞給createCGImage:fromRect:去處理。這會渲染圖像。這種方式有點輕微棘手的事情是,CIImage不具有framebounds;它只有一個extent。你會經(jīng)常使用它作為createCGImage:fromRect:的第二個參數(shù).最終輸出的CGImage可以隨便使用,例如在應(yīng)用程序的顯示,或者轉(zhuǎn)換為一個UIImage,或者進行進一步的繪制。
    這種方法具有一個優(yōu)點是當(dāng)渲染發(fā)生時你有全部的控制權(quán)。但是要注意:創(chuàng)建一個CIContext是非常昂貴的!最好的方式是,事先只創(chuàng)建CIContext一次--整個app中--, 并在每次渲染時重用它。
  • With a UIImage
    在最后的CIImage上調(diào)用的init(CIImage:)init(CIImage:scale:orientation:)直接創(chuàng)建一個UIImage。然后,可以把UIImage繪制到某些圖形上下文中。在繪制發(fā)生的時候,圖像就被渲染了。

蘋果聲稱,你可以簡單地調(diào)用init(CIImage:)創(chuàng)建一個UIImage然后把它賦值給UIImageView的image屬性,那么UIImageView會渲染這個圖像。根據(jù)我的經(jīng)驗,這是不正確的。為了渲染它你必須顯式的繪制它。

為了說明這一點,我會用我自己的一張普通的照片創(chuàng)建一個圓形的帶陰影效果的圖片。我們會先從圖片中導(dǎo)出CIImage。我們對圖片運用一個白色到黑色的徑向漸變的濾鏡。然后,我們使用第二個濾鏡,這個濾鏡把上面的逕向漸變當(dāng)做一個遮罩來把我的照片和一個透明背景色混合:其中徑向漸變?yōu)榘咨u變的內(nèi)半徑)的部分,只會看到我的照片,徑向漸變是黑色(漸變的外半徑)的部分,我們只會看到透明的顏色,在它們中間的環(huán)形漸變區(qū)域內(nèi)圖片漸漸消失。下面的代碼用兩種方式來設(shè)置濾鏡:

let moi = UIImage(named: "Moi")!
let moiCI = CIImage(image: moi)!
let moiextent = moiCI.extent
let center = CIVector(x: moiextent.width / 2.0, y: moiextent.height / 2.0)
let smallerDimension = min(moiextent.width, moiextent.height)
let largerDimension = max(moiextent.width, moiextent.height)
//first filter
let grad = CIFilter(name: "CIRadialGradient")!
grad.setValue(center, forKey: "inputCenter")
grad.setValue(smallerDimension / 2.0 * 0.85, forKey: "inputRadius0")
grad.setValue(largerDimension / 2.0, forKey: "inputRadius1")
let gradimage = grad.outputImage!
//second filter
let blendImage = moiCI.imageByApplyingFilter("CIBlendWithMask", withInputParameters: ["inputMaskImage": gradImage])

現(xiàn)在我們在濾鏡鏈的最后得到了最終的CIImage(blendimage);注意:此時處理器還沒有進行任何渲染。現(xiàn)在我們想要生成最終的位圖并顯示它。例如,我們可以通過UIImageView來顯示它。有兩種不同的方法可以做到這一點。我們可以通過調(diào)用CIContext(options:nil)CIImage傳給我們已經(jīng)事先準(zhǔn)備好的CIContext(self.context)來創(chuàng)建一個CGImage

let moiCG = self.context.createCGImage(blendImage, fromRect: moiextent)
self.iv.image = UIImage(CGImage: moiCG)

另外,我們可以把濾鏡鏈最后輸出的CIImage作為一個UIImage然后把它繪制成一個位圖:

UIGraphicsBeginImageContextWithOptions(moiextent.size, false, 0)
UIImage(CIImage: blendImage).drawInRect(moiextent)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.iv.image = im

效果如下:


通過繼承CIFilter可以把濾鏡鏈封裝成一個自定義濾鏡。子類只需要重寫outputImage屬性(以及可能的其他方法,如setdefaults),并使用額外的屬性使之成為鍵值編碼兼容的對于任何輸入的鍵.。下面是我們的陰影濾鏡一個簡單的CIFilter子類,其中一個輸入的鍵是指定輸入的圖像,另一個輸入鍵是調(diào)整漸變的小半徑的百分比:

class MyVignetterFilter: CIFilter {
    var inputImage: CIImage?
    var inputPercentage: NSNumber? = 1.0
    override var outputImage: CIImage? {
        return self.makeOutputImage()
    }
    private func makeOutputImage() -> CIImage? {
        guard let inputImage = self.inputImage 
        else {return nil}
        guard let inputPercentage = self.inputPercentage 
        else {return nil}
        let extent = inputImage.extent
        let grad = CIFilter(name: "CIRadialGradient")!
        let center = CIVector(x: extent.width / 2.0, y: extent.height / 2.0)
        let smallerDimension = min(extent.width, extent.height)
        let largerDimension = max(extent.width, extent.height)
        grad.setValue(center, forKey: "inputCenter")
        grad.setValue(smallerDimension / 2.0 * CGFloat(inputPercentage), forKey: "inputRadius0")
        grad.setValue(largerDimension / 2.0, forKey: "inputRadius1")
        let blend = CIFilter(name: "CIBlendWithMask")!
        blend.setValue(inputImage, forKey: "inputImage")
        blend.setValue(grad.outputImage, forKey: "inputMaskImage")
        return blend.outputImage
    }
}

下面是如何使用我們的CIFilter子類并顯示其輸出的一個例子:

let vig = MyVignetterFilter()
let moiCI = CIImage(image: UIImage(named: "Moi")!)!
vig.setValuesForKeysWithDictionary([
    "inputImage": moiCI,
    "inputPercentage": 0.7
])
let outim = vig.outputImage!
let outimCG = self.context.createCGImage(outim, formRect: outim.extent)
self.iv.image = UIImage(CGImage: outimCG)

Blur and Vibrancy Views

iOS上的某些視圖,例如導(dǎo)航欄和控制中心會顯示一種半透明的模糊過渡效果。iOS提供了UIVisualEffectView類來幫助你實現(xiàn)這種效果。你可以把其他視圖放到UIVisualEffectView前面,但是任何子視圖應(yīng)該放在contentView里面。通過設(shè)置contentViewbackgroundColor來實現(xiàn)模糊的效果。

使用init(effect:)來創(chuàng)建UIVisualEffectView并使用它;effect:參數(shù)是一個UIVisualEffect子類的實例:

  • UIBlurEffect
    調(diào)用init(style:)來創(chuàng)建一個UIBlurEffect;style參數(shù)(UIBlurEffectStyle)是.Dark.Light.ExtraLight。(.ExtraLight特別適合于一小塊界面,例如一個導(dǎo)航欄或工具欄。):
    let fuzzy = UIVisualEffectView(effect: (UIBlurEffect(style: .Light)))
  • UIVibrancyEffect
    調(diào)用init(forBlurEffect:)來初始化UIVibrancyEffectVibrancy讓視圖和它地下的模糊效果相協(xié)調(diào)。這樣做的目的是,高亮效果視圖應(yīng)該在一個模糊效果視圖的前面,一般在單一的UIViewcontentView里面添加一個高亮效果;告訴高亮效果的下面是什么模糊效果,它們會自己協(xié)調(diào)。您可以獲取的視圖的模糊效果作為其effect屬性,但是這是一個UIVisualEffect類,所以你必須要轉(zhuǎn)換為UIBlurEffect才能把它傳遞給init(forBlurEffect:)

下面是一個模糊效果并覆蓋整個視圖的視圖,而且包含一個含有UILabel的高亮視圖:

let blur = UIVisualEffectView(effect: UIBlurEffect(style: .ExtraLight))
blur.frame = mainview.bounds
blur.autoresizingMasks = [.Flexiblewidth, .FlexibleHeight]
let vib = UIVisualEffectView(effect: UIVibrancyEffect(forBlurEffect: blur.effect as! UIBlurEffect))
let lab = UILabel()
lab.text = "Hello, world!"
lab.sizeToFit()
vib.frame = lab.frame
vib.frame = lab.frame
vib.contentView.addSubview(lab)
vib.center = CGPointMake(blur.bounds.midX, blur.bounds.midY)
vib.autoresizingMask = [.FlexibleToMargin, .FlexibleBottomMargin, .FlexibleLeftMargin, .FlexibleRightMargin]
blur.contextView.addSubview(vib)
mainview.addSubview(blur)

效果如下圖:


蘋果似乎認(rèn)為高亮使視圖與模糊底層結(jié)合時更加清晰,但我不這么認(rèn)為。高亮的視圖的顏色和會模糊的顏色綜合但這會是高亮的視圖更加不清晰。上圖中使用.Dark.ExtraLight模糊效果時label看起來還可以,但是使用.Light就很難看清了。
在UIVisualEffectView.h頭文件中有很多值得研究的額外信息。例如,為了高亮圖片視圖必須使用模版(template)圖片。
在nib編輯器中都可以直接使用模糊和高亮視圖。

==未完待續(xù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內(nèi)容