iOS 動畫 - 窗景篇(一)

iOS 有一種動畫,使用雖然簡單,但能實現很多有趣的效果,那就是 mask 動畫。

如果你還不瞭解 mask 動畫,看完本系列文章後,你可以學會這種動畫。如果你已經使用過了,本文也能幫你梳理一下,讓你使用起來更方便。

本系列文章共3篇,作爲系列的開篇,我們首先要搞清楚一個問題:什麼是 mask。

一、什麼是 mask


mask 是 UIView 或 CALayer 的一個屬性,它決定了 view 或 layer 的哪一部分能被我們看到

本文爲了方便講述,主要使用 view 和它的 mask 屬性。

iOS 對 mask 的描述,對很多人來說不是特別直觀,所以在貼出定義之前,我們先嚐試直觀地看一下。

首先,我們來看一下這張圖:

如圖所示,一張紙上有個圓洞,紙蓋在了左邊的圖片上,圖片的一部分通過這個洞透了過來,就像牆上開了一扇窗,讓我們看到了一部分風景。

不嚴謹的說,中間的這張黑紙就是 mask,它決定了 view 的哪一部分能被我們看到。

不過這張圖會誤導我們,讓我們感覺 mask 擋住了 view,其實並不是這樣。
我們來看一下這張圖

從這張圖中,我們可以看到:frontView.mask 隻影響了 frontView 哪部分可以被我們看到 ,對後面的 backView 沒有任何影響。
看上去,mask 更像是對 view 進行了裁剪。

上面的兩張圖並不符合 iOS 對 mask 的描述, 但通過這兩張圖,我們應該對 “mask 決定了 view 的哪一部分能被我們看到” 這句話,有了直觀的印象。

接下來,我們就一起來看一下 iOS 對 mask 的描述。

二、iOS 中的 mask


我們首先看一下 iOS 對 UIView 的 mask 的定義:

var mask: UIView? { get set }

可以看到,UIView 的 mask,其實就是另一個 UIView。

再看一下這句簡要描述:

An optional view whose alpha channel is used to mask a view’s content.

這句描述指出了:用 mask 的 alpha channel(透明度通道) 去決定 view 的內容顯示,但沒說怎麼決定。

接下來再看一下詳細的描述:

Discussion

The view’s alpha channel determines how much of the view’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through but fully transparent pixels block that content.

這句 “Fully or partially opaque pixels allow the underlying content to show through” 就比較清晰了,大意是:mask 上不透明的部分(包括半透明的部分,這種情況我們稍後再看),可以讓 view 透過來。

“不透明的部分,可以讓 view 透過來”,這句話聽上去可能讓人有點困惑,我們還是用圖來表示一下,我們先根據這個描述,改造一下前文的那張圖,如下:

圖中的 mask(本質上也是個 view),只有中間的圓是有顏色(黑色)的,其餘部分是透明的。當它作爲左邊 view 的 mask 時,只有中間有顏色(也就是不透明)的圓,才允許 view 透過來。

這就是爲什麼有些人覺得 mask 的描述不是很直觀,畢竟我們下意識裏,會覺得透明的部分,才能透過後面的東西。

其實也很好理解,mask 上的不透明的部分,只是窗戶區域的描述,而不是窗戶本身。當它作爲 view 的 mask 時,系統就會把 mask 上不透明的部分(不管是純色、圖像還是視頻等)作爲窗戶區,真正渲染時,就會讓 此處的view 透過來。

爲了方便講述,上面的圖中,view 和 mask 的 我使用了一樣的尺寸,但其實 mask 的 frame 並不重要。view 哪些部分能顯示,只以 mask 不透明區域爲準,和 mask 的 frame 沒有關係 。

比如下面圖中的效果和上圖是一樣的:
在這裏插入圖片描述

注:mask 的 frame 基於 view 的座標系(和該 view 的 subView 的 frame 類似 )

我們知道了 mask 的含義,那麼 mask 具體怎麼使用呢,很簡單,就是把1個 view 賦值給另一個 view 的 mask 屬性。

比如上圖的效果,我們大致可以這樣寫代碼:

// backView
backView.frame = UIScreen.main.bounds
view.addSubview(backView)

// frontView 圖片景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)

// 圓窗
let mask = CircleView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
frontView.mask = mask

至此,我們差不多就理解了 iOS 的 mask。
本系列文章中,爲了便於描述,用窗指代 mask,用景指代 mask 關聯的 view。

那麼接下來,我們就簡單地看一點窗、景的簡單例子,來打開一下思路,思路一打開,後續文章中的效果就很容易實現了。

三、窗、景的簡單例子


先來看一下窗。

我們已經知道,view 的 mask 也是個 view,既然 view 的樣式多種多樣,那窗的樣式當然也是五花八門的。

比如我們用一個 UIButton 作爲之前圖片 view 的 mask,button 的 title 自然就成了文字窗戶,效果如下圖所示:

示意代碼如下:

// 圖片景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)

// 文字窗
let mask = UIButton(type: .custom)
mask.setTitle("柯爛", for: .normal)
mask.titleLabel?.font = UIFont.systemFont(ofSize: 100)
mask.frame = CGRect(x: 0, y: 0, width: 300, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
frontView.mask = mask

接下來再看一下景。

景也是 view,可以是純色、圖片,也可以是動圖、視頻等。
本例中,我們用一個漸變動畫的 view 作爲景,用一個圓形窗,效果如下圖所示:

由於背景是漸變動畫,下面這張動圖能更好地展示效果:

示意代碼如下:

// 漸變動畫景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)
// 執行動畫
frontView.start()

// 圓窗
let mask = CircleView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
frontView.mask = mask

也許有的同學已經想到了,上面的文字窗、漸變景一結合,不就是個不錯的效果嗎,
是啊,這就形成了動態漸變的文字效果,如下面的動圖所示:

示意代碼如下:

// 漸變動畫景
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)
// 執行動畫
frontView.start()

// 文字窗
let mask = UIButton(type: .custom)
mask.setTitle("柯爛", for: .normal)
mask.titleLabel?.font = UIFont.systemFont(ofSize: 100)
mask.frame = CGRect(x: 0, y: 0, width: 300, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
frontView.mask = mask

例子舉到這裏,大家就明白了,只要選窗用景的思路開闊一些,mask 動畫效果是多種多樣的。

細心的同學還記得,前面我們留了個尾巴,我引用一下前文的話:

這句 “Fully or partially opaque pixels allow the underlying content to show through” 就比較清晰了,mask 上不透明的部分(包括半透明的部分,這種情況我們稍後再看),可以讓 view 透過來。

由於完全不透明的 mask 比較好理解(就是把不透明的區域,摳掉當做窗戶),所以前文都以完全不透明的 mask 作爲示例。

讀到了這裏,我們對半透明的 mask 理解已經沒有障礙了,所以我們補充一下半透明 mask。

四、半透明 mask


現在大家已經瞭解了,mask 上的不透明區相當於描述了窗戶的區域,而 mask 的半透明度,相當於描述了窗戶的通透程度。

mask 的區域完全不透明,那窗戶就是全透明的,view 能完全透過來;而 mask 的區域半透明,窗戶就是半透明的,view 能模模糊糊的透過來。
mask的透明度和窗戶的透明度成反比。

我們用一個實踐中常用的例子來看一下。

例子是這樣的,
有時候,我們有一個半屏的 tableView,它的頂部不再屏幕的頂部,而是在屏幕的中央(比如直播間裏的聊天區)。
這種情況下,cell 向上滑出 tableView,滑到一半時,由於只顯示半個 cell,tableView 的邊緣會顯得很明顯。如下圖所示:

我們想讓它的邊緣不那麼明顯,有個類似淡出的效果,如果用 mask 來實現,只要有一個豎直漸變的 view 作爲 mask 就可以了。

注:此處 mask 要作爲 tableView 的 superView 的 mask(可以創建一個和 tableView 大小相等的 view 作爲它的 superView)

mask 頂部逐漸過渡到了透明,相應地,窗戶就漸漸過渡到了不透明,tableView 的頂部看上去就像是淡出了,效果如下圖所示:

示意代碼如下:

// table 景(tableContainer 來輔助)
let tableContainer = UIView()
let bounds = UIScreen.main.bounds
let gradientHeight: CGFloat = 20.0
tableContainer.frame = CGRect(x: 0, y: bounds.midY, width: bounds.width / 2, height: bounds.height / 2)
view.addSubview(tableContainer)
tableView.frame = tableContainer.bounds
tableContainer.addSubview(tableView)

// 半透明漸變窗
let mask = GradientView()
mask.frame = tableContainer.bounds
mask.gradientLayer.startPoint = CGPoint(x: 0, y: 1)
mask.gradientLayer.endPoint = CGPoint(x: 0, y: 0)
mask.colors = [.white, .white, UIColor.white.withAlphaComponent(0)]
mask.locations = [0, 1 - Double(gradientHeight / tableContainer.bounds.height), 1]
// 作爲 tableContainer 的 mask,而不是 tableView
tableContainer.mask = mask

半透明 mask 我們就先說到這,後面的文章,我們主要還是以不透明 mask 爲主。

尾聲


至此,我們對 mask 就有了足夠的瞭解,也打開了一點窗與景的思路;接下來的文章,我們就一起來看一下 mask 的各種玩法。

本文所有示例,在 GitHub 的 WindowAndScenery 庫 裏都有完整的代碼。

感謝您的閱讀,我們下篇文章見。

傳送門


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章