許多UIView的子類,如一個UIButton
或一個UILabel
,它們知道怎么繪制自己。遲早,你也將想要做一些自己的繪制。你可以事先準(zhǔn)備好您的繪圖作為一個圖像文件。您可以用代碼繪制一張圖片在應(yīng)用程序運行中。您可以在`UIView`子類,例如一個UIImageVie
或一個UIButton
中顯示在你的圖像。一個純粹的UIView
有關(guān)于繪畫的一切,繪圖很大程度上取決于你;你的代碼決定view
畫什么,你的界面就是什么。
Images and Image Views
基本的UIKit的圖像類是UIImage
。UIImage
可以讀取磁盤上的文件,因此,如果圖像不需要動態(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 catalog
在 app 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
的圖片將自動使用,與由此生成的UIImage
的scale
屬性值為2.0
。同樣,如果文件具有相同的名稱并且有 @3x
,它擴展將會用于iPhone 6 Plus
,并且 scale
屬性值為 3.0
。
通過這種方式,您的應(yīng)用程序可以包含一個圖像文件在在不同分辨率下的多個版本。由于scale
屬性,圖像的高分辨率版本和單分辨率的版本繪制的大小相同。因此,在高分辨率的屏幕上,代碼不用修改就能工作,但圖片看起來清晰。
同樣,具有相同名稱的由~ipad
擴展的文件當(dāng)app
在ipad
上運行時會被自動使用 。你可以使用這種方式在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
版的圖像,檢查iPhone
和iPad
的圖像集的屬性檢查器,不同的圖片位置將會出現(xiàn)在asset catalog
中。
Asset catalog
也可以區(qū)分圖像在不同size class
下的版本。在圖像集的屬性檢查器中,使用寬度和高度的彈出菜單來指定要區(qū)分哪個size class
。如果我們把運行著app
的iPhone旋轉(zhuǎn)到橫向,如果有既有的圖片集中的Any height
和Compact height
圖像都可以使用的話,會優(yōu)先使用Compact height
版本的圖像。這些功能是實時的在app
運行期間;如果應(yīng)用程序從橫向旋轉(zhuǎn)為縱向,Any height
會自動替換掉Compact height
的圖片,如果圖片集中的2中圖像都可以使用的話。
Asset catalog
這種神奇的能力是通過trait collections
和UIImageAsset
類來實現(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)iPhone
在portrait ori‐ entation
時使用前一個圖像,當(dāng)iPhone
在 landscape 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
放到用戶界面 - 例如,一個UIImageView
的image
屬性 - ,當(dāng)app
改變方向時,它將和smiley
交替顯示。可喜的是,即使是沒有永久引用frowney
,smiley
,或UIImageAsset
(moods
),這都會自動發(fā)生。原因是,frowney
和smiley
由系統(tǒng)緩存(因為調(diào)用init(named:)
),他們各自保持一個它們自己關(guān)聯(lián)的UIImageAsset
的強引用。
通過init(named:inBundle:compatibleWith- TraitCollection:)
從app bundle
或者asset catalog
獲取圖像時可以指定一個目標(biāo)trait collection
。bundle
參數(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
屬性;UIImageView
的highlighted
屬性值決定在任何給定的時間顯示哪幅圖。和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)該注意UIImageView
的clipsToBounds
屬性;如果是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
,并且如果圖像視圖的clipsToBounds
為true
,我們會得到一種漸變的效果,因為拉神完成的圖象的頂部和底部已經(jīng)超出image view
的邊界而不會被繪制出來。
效果如下:
你可以通過項目的asset catalog
而不是代碼來配置一個可調(diào)整大小的圖像。經(jīng)常出現(xiàn)的情況是:一個特定的圖像將在您的應(yīng)用中主要被用來作為一個可調(diào)整大小的圖像,并且總是具有同樣的capInsets
和resizingMode
,所以很有必要只配置此圖像一次,而不是重復(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)用程序的主window
的tintColor
可能是你僅有的能對window
所做的改變之一;否則,你的應(yīng)用程序?qū)⒉捎孟到y(tǒng)的藍(lán)色的tint color
。 (如果你使用的是main storyboard
,可以在文件檢查器中設(shè)置Global Tint color
的顏色。)可以設(shè)置個別view
的tint color
, 這會被他的子視圖繼承。下圖顯示了一個應(yīng)用中主window
的tintcolor
是紅色的,顯示相同的背景圖片的兩個按鈕,一個正常渲染,另一個以模板方式渲染:
在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è)置view
的semanticContentAttribute
防止圖片鏡像。
不幸的是,在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
并在layer
的delegate
里面實現(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
類知道如何繪制它們自己;其中包括UIImage
,NSString
(繪制文本),UIBezierPath
(用于繪制形狀),和UIColor
。有些類提供能力有限但是方便的方法;有些類則非常強大。在許多情況下,UIKit
會滿足你所以的需求。
你只能繪制到當(dāng)前圖形上下文中當(dāng)你使用UIKit
時。所以,如果你在一個UIGraphicsBeginImageContextWithOptions
或drawRect:
情況下,你可以直接使用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)前圖形上下文的引用。
你不必分開使用
UIKit
或Core 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
的任何地方。例如,你可以把它賦值給UIImageView
的image
屬性,從而導(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
UIImage
的Core 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è)備上,如果我們的圖片有一個高分辨率的版本,那么繪制就會出錯。原因是我們使用UIImage
的init(named:)
方法獲取的火星圖片,在高分辨率的設(shè)備上它返回一個更大尺寸的火星圖像,通過設(shè)置圖片的scale
屬性來匹配原始火星圖片的大小。但CGImage
并沒有scale
屬性,而且也不知道圖片的尺寸增加了!因此,在一個高分辨率的設(shè)備上,我們從火星圖片中提取的mars.CGImage
比mars.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 X ,OS 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)前的圖形上下文中. (此方法比CALayer
的renderInContext:
速度快很多;不過,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
CIFilter
和CIImage
中的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
,并且其他有用的類,如CIVector
和CIColor
,其使用很容易理解。
CIFilter
鍵中輸入的圖像或被濾鏡操作的圖片必須是CIImage
。可以從CGImage
中獲取CIImage
通過調(diào)用init(CGImage:)
或者從UIImage
中獲取CIImage
通過調(diào)用init(image:)
。
不要嘗試直接從
UIImage
的CIImage
屬性獲取CIImage
。此屬性不會把UIImage
轉(zhuǎn)換成CIImage
!它僅僅只是指向支持(back
)UIImage
的CIImage
,如果這個UIImage
真的是被CIImage
支持(back
)的;但是有可能你的圖片不是由CIImage
支持的,而是由一個CGImage
。
然而你可以從濾鏡的輸出中獲取CIImage--這意味著濾鏡可以鏈?zhǔn)秸{(diào)用。
有三種方式來描述和使用濾鏡:
- 通過
CIFilter
的init(name:)
創(chuàng)建濾鏡。通過重復(fù)調(diào)用setValue:forKey:
方法或者通過調(diào)用setValuesForKeysWith- Dictionary:
來為濾鏡添加鍵值對。通過濾鏡的outputImage
獲取CIImage
。 - 通過調(diào)用
CIFilter
的init(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
不具有frame
或bounds
;它只有一個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è)置contentView
的backgroundColor
來實現(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:)
來初始化UIVibrancyEffect
。Vibrancy
讓視圖和它地下的模糊效果相協(xié)調(diào)。這樣做的目的是,高亮效果視圖應(yīng)該在一個模糊效果視圖的前面,一般在單一的UIView
的contentView
里面添加一個高亮效果;告訴高亮效果的下面是什么模糊效果,它們會自己協(xié)調(diào)。您可以獲取的視圖的模糊效果作為其effect
屬性,但是這是一個UIVisualEffec
t類,所以你必須要轉(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ù)