在iOS 7中,一個重大的改變就是隨處可見的虛化,這在通知中心和控制中心表現得尤為搶眼:
然而,當開發者們著手去將類似的模糊效果加入自己的App的時候,他們會發現有相當嚴重的障礙。那時蘋果所界定的設備可用范圍相當簡單,並不強大到足以支持在第三方應用中實現實時模糊。並聲稱開發者們很可能在App裡濫用虛化從而嚴重影響用戶體驗。
不過,精明又狡猾的程序員們很快的創造了自己基於模糊靜態圖片方法來破解實時模糊的算法。
大部分解決方案都效果卓越。不過,之後的iOS 8在開發者工具箱中添加了官方的模糊效果,不僅相當高效,而且其使用的簡單程度讓人驚歎。
提示:想知道如何使用靜態模糊圖片來模擬實時模糊的話可以參考這篇博文。
想要使模糊效果顯得美觀而又高效需要一定技巧,在這一節你將會了解到最常見的模糊算法以及如何使用模糊效果來提升你App的用戶體驗。
模糊的對象是圖片,想要實現模糊,你需要對圖片中的每一個像素使用模糊算法,這樣會得到一個對原圖進行了均勻模糊後的圖片。模糊算法可以在模糊的風格和模糊的復雜度上有很多變化,不過在這個教程裡你將會運用到一個最為常見而且頗為出名的算法——高斯模糊。
模糊算法通常會檢索圖片的每一個像素點並基於它周圍的像素點來計算該像素在模糊後的灰度值。比如,我們想象一張如下所示網格圖:
每一個小格子代表了一個獨立的像素,每個像素點有一個介於1和10之間的值。假設我們要對中心的像素點進行模糊化,那就需要計算四周八個像素中的值的算術平均數,並將這個數作為中心像素的值插入進去。結果如下圖:
接著對原圖的每一個像素點都重復同樣的操作(編者按:原圖中每一個像素的新值應該插入到一張新圖片相應位置的像素中去以免出現錯誤,原圖的像素值依舊不變,原作者並未提示這一點)。
上面的模糊例子僅僅用每個方向上的一個像素單位來進行計算新圖片的像素值,你可以擴大模糊所要采用的像素半徑來提升圖片的模糊效果,如下圖所演示的這樣:
提示:一般說來,使用的模糊半徑越大則處理圖片時候的計算量會越多。iOS會將大部分圖像處理工作交給GPU來處理以確保主線程不會被卡死。
人總是會不由自主的被那些對焦准確的部分而忽視掉被虛化的部分。不管你信不信,這是大自然的道理,因為人眼就是這麼工作的。眼球的對焦機制好像一個調節器一樣捕捉那些離你忽遠忽近的物體,這樣才能讓你感受到周圍一切事物的深度和距離。
App設計師實際上通過模糊掉那些無關緊要的內容來引導用戶的目光關注那些沒有被模糊掉的要素,比如時下流行的Twitter客戶端就是一個很好的示例:
上圖中背景裡的用戶界面能夠勉強識別,因而為用戶提供了一個情景意識來讓他們知道正處於導航層中的哪個位置。在這個例子中用戶只需要選擇一個賬戶登入,就可以退回到沒有被模糊的背景圖層裡去。
提示:雖然模糊能帶給人非常清新的視覺體驗,不過也切忌在你的App中過度使用,因為過度使用或者使用不當都會分散用戶的注意力或者惹惱用戶。
遵照標准的模糊設計方案來讓用戶關注到你想要給出的事物,這樣你就不容易弄糟。你可以在蘋果iOS開發者中心的iOS Human Interface Guidelines文檔中的Designing for iOS章節了解到更多內容。
為了理解如何實現模糊,你需要嘗試在一款以新格林童話故事為藍本的App上添加合適的模糊效果,這款App叫做Grimm。
該應用為用戶提供了一系列的童話故事,當用戶點開某個童話時,它就會在屏幕上顯示完整的故事內容。用戶可以自定義顯示的字體、文本對齊,以及適用於日間或夜晚閱讀的顏色主題。
現在開始你需要下載一個初始工程,在Xcode中打開Grimm.xcodeproj,然後打開Grimm.storyboard看一下App中的視圖控制器,像下面這樣:
你可略過上圖中最前面的那個視圖控制器,因為它在App中只不過是個簡單的底層導航控制器。你需要關注的是後面有編號的視圖控制器:
1.第一個控制器是StoryListController,是用於顯示數據庫中所有童話故事的列表。
2.當你點擊一個童話故事時就會切換到這個視圖控制器StoryViewController,它會顯示選中童話的標題和文本內容。
3.最後的OptionsController是包含在StoryViewController中的,會列出一些可用的字體、對齊、顏色選項。只需要在StoryViewController中輕擊設置圖標就能顯示它。
構建並運行,你就會看到如下所示的一個初始界面:
你可以體驗一下這個應用,選好童話之後,點擊省略號喚出選項視圖來切換不同的字體和閱讀模式,這樣可以了解用戶界面的基本功能。
提示:你可以在模擬器或者除了iPad 2之外的iOS 8設備上運行這個應用。出於性能上的考慮蘋果限制了在iPad 2上顯示模糊效果,App本身的確能很好的運行在iPad 2上,只不過你會看不到任何惬意的模糊效果而已。
眼尖的同學可能會發現在這個工程裡面還殘留有Objective-C代碼。
為此焦慮大可不必,這一段Objective-C代碼在很多應用工程裡面都有用到,而且還相當堅挺。它的作用是在你的所有Swift文件中接入Grimm-Bridging-Header.h頭文件,因為我們在這裡沒有必要再單獨為Swift重寫一個。
提示:Swift被設計得能夠良好的兼容Objective-C,這樣的話包括蘋果自己的開發人員在內的開發者能夠直接在工程裡添加Swift代碼而免去重構代碼的麻煩。連接了頭文件之後你就可以在你的Swift文件中寫進Objective-C代碼了。
在項目資源管理器中打開GrimmCategoriesUIImage+ImageEffects.m文件,略過前面所有的注釋來看看形如applyBlurWithRadius:tintColor:saturationDeltaFactor:maskImage:的代碼段,本教程從頭到尾都不會覆蓋或是修改這些代碼,但是讀一讀有助於你理解其中包括哪些基本功能。
在iOS 7發布的時候蘋果還提供了UIImage類來演示如何如何對圖片應用靜態模糊。這充分的發揮了Accelerate框架在使用向量和矩陣運算上的優勢,使得在圖像處理上使用這些計算時變得更為方便。
applyBlurWithRadius:tintColor:saturationDeltaFactor:maskImage:這裡的參數有模糊半徑、飽和度、以及可選的掩蓋圖片。該方法會運用大量的數學運算生成一張處理後的新圖片。
在你使用你的模糊效果前你需要獲取一張快照,今天你的大部分力氣將會花在StoryViewController視圖底部的繪制選擇上。
打開StoryViewController.swift文件並找到setOptionsHidden方法,在這裡你會先獲取整個StoryViewController控制器的截圖,然後在將其模糊化之後作為選項界面的背景圖片。
把下面這個方法添加到setOptionsHidden方法前面:
func updateBlur() {
//為了避免在截圖的時候截到選項界面,因此先要確保選項界面必須是隱藏狀態。
optionsContainerView.hidden = true
//創建一個新的ImageContext來繪制截圖,你沒有必要去渲染一個完整分辨率的高清截圖,使用ImageContext可以節約掉不少的計算量
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, true, 1)
//將StoryViewController中的界面繪制到ImageContext中去,因為你需要確保選項界面是隱藏狀態因此你需要等待屏幕刷新後才能繪制
self.view.drawViewHierarchyInRect(self.view.bounds, afterScreenUpdates: true)
//將ImageContext放入一個UIImage內然後清理掉這個ImageContext
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
在點擊省略號之後你需要調用一個updateBlur方法來模糊截圖,這樣你需要在setOptionsHidden方法的一開始添加如下代碼:
if !hidden {
updateBlur()
}
更進一步之前,你應該檢查一下你是否截到你想截的那張圖。
在你的上一步添加的updateBlur方法源碼中找到UIGraphicsEndImageContext()這一行並添加一個斷點,然後構建並運行,選擇一個童話故事並打開它。
一旦童話打開就點擊省略號來觸發斷點。在調試欄裡展開screenshot變量然後選中如下嵌套在其中的some變量
敲擊空格鍵來打開Quick Look,你應該會看到一張故事欄的非高清截圖。如下所示:
請注意在截圖中並未包括UINavigationController中的任何元素,因為故事列表的視圖是作為UINavigationController的背景圖存在的,導航控制器則位於截圖的區域之外了。
現在你已經能截到一張正確的快照了。你可以使用我們之前提到的UIImage類來對你的截圖開始進行模糊化。
仍舊打開StoryViewController.swift文件,找到你剛剛更改過的updateBlur方法,在最後一行UIGraphicsEndImageContext()的下面添加這行代碼:
let blur = screenshot.applyLightEffect()
移動你剛剛加在文件裡的斷點,像這樣:
提示:你可以在滾動槽裡面拖著斷點上下移動。
構建並運行,打開一則童話故事,點擊導航器裡面的省略號,然後在調試欄裡面找到blur變量並使用空格打開Quick Look。
稍等……blur裡面好像什麼都沒有?去哪了?
你沒有看到任何東西是因為你的斷點恰好放在了blur變量設置的那一行,這樣Xcode會停在這一行執行之前的一步。
想要執行下圖中高亮的那一行你可以敲擊F6或者如圖中所示點擊執行下一步:
現在你可以展開blur變量了,選擇底下的那個some變量然後敲擊空格鍵喚出Quick Look查看你模糊化後的圖片:
提示:LLDB(Xcode的調試器)有時候並不是很適宜用於Swift,所以你可能會需要點兩次執行下一步才會顯示一個some變量。
你現在可以獲取一張快照並且執行模糊化了,接下來要做的就是在App中加入這張模糊後的圖片了。
打開StoryViewController.swift文件在屬性定義的那堆代碼的開始加入下面這行:
var blurView = UIImageView()
這裡可以為每個StoryViewController實例初始化一個UIImageView。
找到viewDidLoad方法並在這個它的最後加上這樣一段:
optionsContainerView.subviews[0].insertSubview(blurView, atIndex:0)
在Grimm.storyboard中把OptionsController放進了一個視圖容器以方便用戶點擊省略號時候就顯示出來。因為你無需直接使用OptionsController所在圖層,你要做的就是獲取這個容器的subview,在這種情況下這層view只是恰好屬於OptionsController。最後你需要把那個模糊的blurview作為subview添加到視圖堆棧的最底部,保證它處於其他所有視圖的下方。
在StoryViewController.swift文件中找到updateBlur方法在最後添加如下代碼:
blurView.frame = optionsContainerView.bounds
blurView.image = blur
optionsContainerView.hidden = false
因為blurView在Storyboard中並沒有被設置過,所以它會有一幀CGRectZero的圖片,除非你有手動設置過。當然你也可以設置你剛剛模糊生成的那張圖片的屬性。
這裡還要注意的是你在截圖之前曾經把optionsContainerView設置為不可見的隱藏狀態,一定要記得在虛化方法完成的最後將optionsContainerView設置為可見。
取消你之前設定的斷點,構建並運行,在選擇了一則童話之後點擊設置選項,注意看著它范圍內的模糊效果,如下:
這一個虛化看上去還是有點猥瑣,因為它好像跟後面的文本並不是很搭配?
在默認情況下,UIImageView會重置圖片的大小以確保和視圖中的畫面適應,也就是說那張大一些的虛化圖片已經被壓縮小了。所以就產生了這樣的效果。
為了修正這一錯誤,你需要把UIImageView的contentMode屬性改為除了默認的UIViewContentMode.ScaleToFill外的其它值。
在updateBlur中設置blurView那一行的下面貼上這些代碼:(注意B是大寫的)
blurView.contentMode = .Bottom
UIViewContentMode.Bottom表示強制讓圖片保持原有大小,而不是僅有只有UIImageView原圖本身的中下部那麼大。
構建並運行,現在看看虛化的效果如何了?
在你的靜態模糊准備拿去使用之前你還需要多考慮一個事,旋轉你的設備或者虛擬機(command+左/右方向鍵),你可以看到視圖的大小並沒有被重置。
因為你的所有文本采用了自動布局,所以之前的截圖不再有用了,你需要在旋轉之後重新截圖快照並且更新一下blurView。
這個很簡單就可以實現,在StoryViewController.swift重寫一下下面這個方法:
override func viewWillTransitionToSize(size: CGSize,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
// animateAlongsideTransition方法可以使你旋轉屏幕的時候的變化更為動感並且在旋轉完成後作一些清理,你僅僅需要的是後者,因為你還需要截下optionsViewController旋轉之後的一幀圖。
coordinator.animateAlongsideTransition(nil, completion: { context in
// 在旋轉後更新一下blurView,這樣就會使用新的布局了
self.updateBlur()
})
}
構建並運行之後試著改變一下設備或者模擬器的角度,會發現有新的布局了:
模糊范圍的大小正確無誤,不過還不夠。滑動後面的文本區你會發現虛化部分沒有發生任何改變。
根據上面的經驗你也應該知道該怎麼修改。而之後的iOS 8提供了動態生成虛化的工具。應用中采用實時模糊效果這一事從開發者們在iOS 7上開辟的解決方案以來那是說來話長了。
iOS 8 提供了一套完整實用的虛化工具。UIVisualEffect的子類UIBlurEffect正是我們所感興趣的。UIBlurEffect提供了你在導航欄、通知中心和控制中心裡看到的那些漂亮的虛化,你也可以在你的App中使用這個效果。
打開StoryViewController.swift文件之後找到setOptionsHidden方法,如果你之前在第一個if條件分支裡面寫入過updateBlur,那就將它注釋掉。修改後如下:
雖然你做完了,但是你不能完全保證blurview沒有被添加到場景中去,注釋掉下面這一行:
optionsContainerView.subviews[0].insertSubview(blurView, atIndex:0)
提示:不要只是簡單的刪除掉那些代碼,你只需注釋掉就好,這樣也便於你在回顧的時候發現有什麼不同。如果你對你手動添加的模糊代碼沒有任何想法,那你也可以刪掉它們而非注釋。
構建並運行之後你會發現除了你的虛化不見了而外剩下的部分都能正常運行。
打開Grimm.storyboard然後找到Options Controller Scene,選擇view,展開Attributes Inspector然後更改view的background為Clear color,如下:
打開OptionsController.swift文件在viewDidLoad方法中加入下面代碼,位置就在你之前添加過的optionsView的後面:
// 創建一個樣式為UIBlurEffectStyle.Light的UIBlurEffect,定了要應用的效果,其他的效果樣式還有UIBlurEffectStyle.ExtraLight和UIBlurEffectStyle.Dark
let blurEffect = UIBlurEffect(style: .Light)
// 創建一個UIVisualEffectView並為其設置需要使用的效果。UIVisualEffectView是UIView的子類,在這裡單獨用來定義和顯示復雜的虛化效果。
let blurView = UIVisualEffectView(effect: blurEffect)
// 解除blurView自適應遮罩大小限制的變化,過會兒你也可以手動添加限制,然後將它至於視圖堆棧裡的最下面。如果你把它加入了最上方,它會把所有的控制器都遮在下面。
blurView.setTranslatesAutoresizingMaskIntoConstraints(false)
view.insertSubview(blurView, atIndex: 0)
原文是按照上面寫的,但測試後發現,背景是透明的,修改過後如下:
現在你需要確保你的blurView能夠適宜的布局。
constraints.append(NSLayoutConstraint(item: blurView,
attribute: .Height, relatedBy: .Equal, toItem: view,
attribute: .Height, multiplier: 1, constant: 0))
constraints.append(NSLayoutConstraint(item: blurView,
attribute: .Width, relatedBy: .Equal, toItem: view,
attribute: .Width, multiplier: 1, constant: 0))
仍然是在viewDidLoad中,在addConstraints的調用之前寫入下面代碼:
構建並運行。打開童話故事點擊省略號,然後滑動後面的文本,會發現虛化部分能夠實時變化了:
虛化的效果相當棒——不過蘋果像以前一樣對其進行了提升。結合使用UIVibrancyEffect與UIVisualEffectView可以調整文本的顏色使得App看上去更加艷麗。
下面這張圖展示了Vibrancy在背景圖片完全相同的情況下如何讓你的標簽和圖標在屏幕上顯得更為舒適:
左邊的顯示的是通常情況下的標簽和按鈕,而右邊的顯示的是應用了Vibrancy之後的效果。
提示:UIVibrancyEffect必須添加到已經用UIBlurEffect配置過的UIVisualEffectView中去,否則就不會有任何的虛化圖片會應用Vibrancy效果。
// 使用你之前設置過的blurEffect來構建UIVibrancyEffect,UIVibrancyEffect是UIVisualEffect另一個子類。
let vibrancyEffect = UIVibrancyEffect(forBlurEffect: blurEffect)
// 創建UIVisualEffectView來應用Vibrancy效果,這個過程恰巧跟生成模糊圖一樣。因為你使用的是自動布局所以在這裡需要把自適應大小改為false
let vibrancyView = UIVisualEffectView(effect: vibrancyEffect)
vibrancyView.setTranslatesAutoresizingMaskIntoConstraints(false)
// 將optionsView添加入vibrancyView的contentView屬性裡,這樣就能確保所有的控制視圖都會應用Vibrancy效果
vibrancyView.contentView.addSubview(optionsView)
// 最後你需要在blurView的contentView裡加入vibrancyView來完成效果
blurView.contentView.addSubview(vibrancyView)
最後一件事就是為Vibrancy視圖設置自動布局的限制,這樣就可以與你的控制器視圖保持一直的高寬。
把下面的限制加入viewDidLoad方法的最後:
constraints.append(NSLayoutConstraint(item: vibrancyView, attribute: .Height, relatedBy: .Equal, toItem: view, attribute: .Height, multiplier: 1, constant: 0))
constraints.append(NSLayoutConstraint(item: vibrancyView, attribute: .Width, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1, constant: 0))
構建並運行,喚出設置選項來看看你的Vibrancy效果。
除非你的眼睛也是高分屏的,不然真的很難看清標簽和控制器,那麼究竟發生了什麼?
這個情況事實上是這樣的,因為你blurView使用的樣式是UIBlurEffectStyle.Light,所以導致它是白色的。這樣的話就不能產生意料之中的Vibrancy效果了。
在viewDidLoad方法中把blurEffect的初始化改為下面這樣:
let blurEffect = UIBlurEffect(style: .Dark)
這樣就改變而且增加了模糊視圖與背景之間的顏色反差。
構建並運行之後你就能看到一個稱心如意的Vibrancy效果了。
你可以在這裡下載到完成後的工程。
至此你已經知道如何手動模糊一張圖片了,也學會了如何進行實時的模糊渲染,也會在你的App上簡單使用UIVisualEffectViews。
你所能使用的模糊技巧僅限於靜態圖片,所以圖片不會很生動而且不能實時的更新。不過使用UIBlurEffect卻可以進行實時更新,這樣你就可以借助這個效果做一些奇妙的事,比如說做動畫。
同時你也可能會企圖把所有的東西都來模糊一下——請想想我們在教程的最初就提到過的事——使用虛化要適當而且有節制。當然對於Vibrancy也是這樣。
(按:這是一篇來自於Ray Wenderlich上有關於iOS 8 虛化效果的教程,在這裡僅僅是一個概覽式的介紹,如果想了解更多,可自行參閱該網站的iOS 8 Feast專題)