聊聊 Material Design 裡,陰影的那些事兒!

承香墨影發表於2017-11-01

當你的設計師要求你在某個 View 上增加陰影效果,那你只需要認真閱讀本文,陰影的問題就不再是問題。

一、前言

設計師的世界,與常人不同,有時候想要扁平化的風格,有時候又想要擬物化的風格。而在 Material Design 出來之後,為 UI 元素引入了高度的概念,它可以讓某個元素更為突出,顯示出它的重要性,更讓人有點選的慾望。

在擬物化的設計裡,UI 元素的高度,反應在效果上,就是在邊框上有陰影的效果,感覺它是距離底部有一個層次的關係。在 Material Design 的設計中,也大量的使用了 陰影 的效果,例如:FloatingActionButton、CardView 這些控制元件,都是預設支援陰影效果的。

如果你想了解 Material Design 中,更多關於陰影的設計,可以查閱官方文件。

material.io/guidelines/…

接下來,我們就來介紹一下,在 Android 的不同版本中,使用不同的方式,去實現陰影的效果。

先來看看實現的效果,雖然多,但是它們實現的方法都不相同。

/all_5.jpeg
/all_5.jpeg

二、陰影的效果

在擬物化的世界裡,陰影主要是對三維空間中的 Z 屬性進行操作。下面是官網的介紹。

由 Z 屬性所表示的檢視高度將決定其陰影的視覺外觀:擁有較高 Z 值的檢視將投射更大且更柔和的陰影。 擁有較高 Z 值的檢視將擋住擁有較低 Z 值的檢視;不過檢視的 Z 值並不影響檢視的大小。

陰影是由提升的檢視的父項所繪製,因此將受到標準檢視裁剪的影響,而在預設情況下裁剪將由父項執行。

developer.android.com/training/ma…

靜態效果如下:

/shadows-depth.png
/shadows-depth.png

再加上,動態的效果應該更能讓你對陰影有所理解。

三、使用標準 Api

Material Design 首次出現在 Android 5.0 中,之後又有一些 Support 包,讓更低的版本,對 Material Design 進行支援。

而在 Api Level 21 之中,增加了兩個屬性 :

  • elevation:高度,用於提升 UI 元素高度的屬性。
  • translationZ:Z 軸的變換效果。

這兩個屬性,有對應的 xml 屬性和 setXxx() 方法,而 Z 軸的改變,主要是由這兩個屬性決定的。

Z = elevation + translationZ

所以,如果你的 App 的 minSdkVersion 就是 21 的話,直接使用這兩個屬性是最優的解決辦法。

3.1 elevation 屬性

elevation 屬性,主要用於給 View 增加一個高度,可以直接被加在 View 控制元件上,呈現在介面上,就是一個帶陰影的效果。

在 layout-xml 佈局中,可以通過 android:elevation 屬性來設定,而在 Java 程式碼中,通過 View.setElevation() 方法來使用它。

直接使用 elevation 屬性設定即可,它接收一個高度的引數,只需要按我們的需要配置即可。

需要注意的是,View 的陰影一定是需要有背景的 View 在視覺上增高之後,投射出來的。也就是類似於打光的陰影效果。簡單來說,就是需要為 View 設定一個 Background,可以使用 android:background 屬性或者 View.setBackground() 方法設定,否者 elevation 的屬性設定將無效。這裡的 Background 只需要設定一個 Drawable 即可,你當然也可以選擇一個圖片或者一個純色的 了。

下面來看看 elevation 屬性的效果:

/setElevation.png
/setElevation.png

往深裡再看看 elevation 屬性的實現方式。

/setElevationMethod.png
/setElevationMethod.png

它最終還是呼叫的 mRenderNode 去做的操作,在追蹤下去,就會發現它底層是用的 native 的方法實現的,所以應該不是我們所理解的用 2D 的漸變模擬陰影的效果。

3.2 translationZ 屬性

translationZ 屬性,主要用於給 View 增加一個在 Z 軸上的變換效果。它和 elevation 配合起來,就是一個一加一等於二的效果。也可以用於設定 View 的高度。

在 layout-xml 佈局中,可以通過 android:translationZ 屬性來設定它,而在 Java 程式碼中,可以通過 View.setTranslationZ() 方法來使用它。

一般來說,我們可以直接使用 android:translationZ 屬性來設定 View,當你配合 android:elevation 屬性一起使用的時候,它們對 View 的高度是累加的,當然你也可以只使用其中一個屬性。

而看到 translationZ 這樣的屬性,很輕易就聯想到了 translationX 和 translationY 了,它們實際上就是不同維度的設定,思路上很像,但是原理不同。對 X、Y 軸的操作並沒有 Api Level 的限制,這一點需要清楚。

和 elevation 屬性一樣,translationZ 也是需要配合 Background 的設定才會生效的,這個應該不難理解。

下面我們來看看 translationZ 屬性的設定效果:

/setTranslationZ.png
/setTranslationZ.png

使用 translationZ 屬性實現的效果,看著和 elevation 的效果很像,而它內部也是依賴於 mRenderNode 去做的實現。

3.3 ViewCompat 來相容 Api

前面就已經提到,當你的 minSdkVersion 達不到 elevation 和 translationZ 這兩個 Api 的要求,設定為 Api Level 21(Android 5.0) 以下。你在使用這兩個屬性的時候,會給你提示 Warning,如果打包的時候有 Lint 的校驗,也是會提示並且導致打包失敗的。

不過看提示你也能發現到底是什麼問題:

Attribute elevation is only used in API level 21 and higher

如果已經明確在低於 Api Level 21 之下的版本,都不加陰影的效果,你可以在佈局中,使用 tools:targetApi="lollipop" 來消除這個 Warning。

如果你是在 Java 程式碼中,為 View 動態設定 elevation 或者 translationZ 屬性的話,除了使用 Build.VERSION_CODES.LOLLIPOP 判斷之外,還可以使用 ViewCompat 這個 Android 為我們提供的標準的 View 相容類,當然,這裡推薦使用 ViewCompat。

既然要用到 ViewCompat 的話,那我們來看看它的原理是什麼。

/ViewCompatImpl.png
/ViewCompatImpl.png

在 ViewCompat 中,會有很多個實現了 ViewCompatBaseImpl 的介面類,它們分別對應了不同的 Api Level ,會在靜態程式碼塊中,根據當前執行裝置的 Api Level ,做不同的實現。而這些,都是高版本繼承低版本的實現,來達到繼承相容的效果。

ViewCompatBaseImpl 這個介面中,定義了很多關於 View 的操作 Api ,這些 Api 都是存在不同的 Api 版本限制的。

在 Api Level 21 中,本身就已經支援了這兩個屬性,也就不存在相容性的問題了,所以它其中會直接呼叫 setElevation()setTranslationZ() 方法。

/ViewCompatV21.png
/ViewCompatV21.png

那麼,我們只需要關心 Api Level 21 以下的實現。通常來說,我們做相容處理,一個方案就是在低版本上,使用一些只在低版本上存在 Api,來對高版本的效果進行模擬;另外一個方案就是放棄低版本,完全對它不做任何處理。

我們來看看 ViewCompat 是對 Elevation 是選用的那個方案。其實 Api Level 21 之下,都沒有對這兩個屬性的操作方法,做任何的處理,你一路追蹤下去可以追蹤到 ViewCompatBaseImpl 。

/ViewCompatBaseImpl.png
/ViewCompatBaseImpl.png

從這裡可以看出,ViewCompat 沒有對這兩個方法做任何的相容,在低版本上,沒有做任何的操作,這也導致了你如果使用 ViewCompat 的話,在低版本上是不會有陰影的效果的。沒有就是沒有,這裡就不再單獨展示了。

那看看使用 ViewCompat 在高版本上的效果圖,其實和之前的也沒啥區別,不過擺在一起看更清晰一些。

/Api-demo-pic.png
/Api-demo-pic.png

3.4 標準 Api 小結

到現在你也能看到,如果不在意 Api level 的話,你完全可以使用 android:elevationandroid:translationZ 兩個屬性來做的陰影的效果,效果也是非常好的,而且它的陰影實際上是不佔用 View 的佈局大小的,它會在原本的佈局之外,向外擴散,所以也不會影響 View 本身大小的視覺效果。

不過它也有缺陷,你只能通過設定這兩個屬性來調整陰影的大小,沒辦法做到精確掌控,並且無法修改陰影的顏色。

最新的 Android 版本市場佔有率,你可以在這個網站上查到。

developer.android.com/about/dashb…

截止到本文編寫的時候,低於 5.0 的版本,差不多在 20% 左右,是否對這部分使用者,放棄陰影的效果,取決於你的產品和設計師。

/Android-level.png
/Android-level.png

如果你需要相容低版本的裝置,後面介紹的一些方法,都可以做到,繼續往下閱讀吧。

四、使用9Patch圖

4.1 什麼上 9Patch 圖

如果你需要相容低版本的 Android 裝置,使用 android:elevation 和 android:translationZ 是無法做到的,它們會在低版本上失效,完全沒有效果,當然前提是你需要做好 Warning 的處理。

而這種陰影的效果,使用 .9圖,也是一個不錯的選擇。

.9 圖 就是 9Patch, 引用官網的介紹:

Draw 9-patch 工具是 Android Studio 中包含的一種 WYSIWYG(所見即所得)編輯器,利用此工具,您可以建立能夠自動調整大小以適應檢視內容和螢幕尺寸的點陣圖影像。影像的選定部分可以根據影像內繪製的指示器在水平或豎直方向上調整比例。

developer.android.com/studio/writ…

4.2 使用 9Patch 設定陰影

直接製作一個帶陰影效果的 .9 圖片,然後設定好內容區域和拉伸區域,就可以在其中模擬出陰影的效果。

舉個例子,使用一個 .9 圖,然後設定在 ImageView 上的背景。

/9patch.png
/9patch.png

在 layout-xml 上,只需要給 ImageView 設定好 android:background 就可以了。

/9patch-xml.png
/9patch-xml.png

來看看它實現的效果:

/9patch-xiaoguo.png
/9patch-xiaoguo.png

使用 .9 圖設定的陰影,效果一般都是有保障的。不過它會作為 View 的背景被設定,所以陰影上佔據 View 的大小的,所以使用圖片模擬出來的陰影,View 本身的視覺效果會小。

放張單圖,可能看不出效果,將一個使用 ViewCompat 實現的效果,放在一起,你就可以看到對比的效果。

/9patch-duibi.png
/9patch-duibi.png

這裡,兩個 ImageView ,實際設定的大小,都是 100dp,但是視覺上,使用 .9 實現的效果,視覺效果就會小。

4.3 快速製作 9Patch

.9 的圖,一般都是設計師會提供給我們。這裡也推薦一個可以製作陰影效果的線上工具。

inloop.github.io/shadow4andr…

通過這個工具,你可以對 .9 圖做各種調整,例如:圓角、陰影的大小、陰影的顏色等等,都是非常方便的設定。前面例子中使用的 .9 檔案,就是使用此工具製作的。

/9patch-web.png
/9patch-web.png

還有一種方式,就是使用 這個層級的 Drawable 去模擬陰影,等於一層一層的疊加。不過使用這種方式太麻煩了,而且效果也很難做到非常的好,一般也不推薦。

4.4 9Patch 模擬陰影小結

使用 .9 圖,製作陰影,基本上不需要擔心效果的問題,使用起來也非常的方便。唯一的問題就是它的陰影部分,會佔用 View 本身的大小,導致 View 在視覺上縮小。

總結來說,它的優點:

  1. 實現方便,只需要設定背景即可。
  2. 陰影的效果可控,顏色、圓角、陰影大小都是可以調整的。

它的缺點也非常的明顯:

  1. 為了讓 View 在視覺上和效果圖匹配,需要預留出陰影的空間。

五、使用 FAB 的原理模擬陰影

我們知道,在 Android 對 Material Design 的效果中,有一些控制元件,就是自帶陰影效果的,並且它也是對低版本相容的。例如:FloatingActionButton 、CardView 等。

那麼,本小結就來看看 FloatingActionButton 實現陰影的原理。

5.1 FAB 的陰影原理

就 FAB 這種有 Support.design 包支援的控制元件,一般都有對 不同的 Api Level 做支援處理,在 FAB 之中也是一樣的,它會根據不同的 Api Level 實現不同的邏輯。

/fab-createImpl.png
/fab-createImpl.png

可以看到,這裡會根據 21、14、<14 三個條件,分別使用不同的實現類,它們內部實際上實現的都是相同的功能。

如果仔細觀察這些 FAB 不同版本的實現類的原始碼,你可以發現它的陰影效果,都是基於一個 ShadowDrawableWrapper 這個 Drawable 來實現的。

例如在 FloatingActionButtonGingerbread 中,就有這樣一段設定背景的程式碼。

/fab-gingerbread-background.png
/fab-gingerbread-background.png

這裡完全上依賴 ShadowDrawableWrapper 來做的陰影效果。

不過 ShadowDrawableWrapper 被宣告的可見性為包內可見,所以我們沒有辦法直接使用它。

/fab-shadowdrawable-class.png
/fab-shadowdrawable-class.png

不過,鑑於 support.design 包中的類,一般都是為了相容做處理,這裡我們只需要將它和它實現的介面 DrawableWrapper 這兩個類,拷貝出來,就可以直接使用了。它們的原始碼都在 android.support.design/widget 包下面,非常容易找到。

它的原理是在你本文需要設定的 Drawable 之外,再包裝一個 Drawable ,然後在這個包裝的 Drawable 上繪製陰影。

繪製的程式碼挺多的,這裡就不貼程式碼了,有興趣可以看看它的原始碼,主要關注 drawShadow() 方法即可。

而如果你在拷貝原始碼的時候,應該能發現,它實際上是可以支援改變陰影的顏色的,如果你有這種需求,只需要再擴充套件它的構造方法,或者直接在 colors.xml 中配置對應的顏色,它設定顏色地方如下。

/shadowWrapper-method2.png
/shadowWrapper-method2.png

可以看到,它主要用三個顏色來做一個漸變的陰影效果。

5.2 使用 FAB 的原理模擬陰影效果

前面說的,我們只需要將 ShadowDrawableWrapper 和 DrawableWrapper 這兩個檔案複製到我們的工程內,稍微修改一下它們的依賴關係。

/fab-project-path.png
/fab-project-path.png

如果直接拷貝原始碼,你會發現它還依賴三個顏色,分別是用於設定陰影的顏色的,這個前面也提到過。一般而言,我們不需要設定它,直接從原始碼中將它們拷貝出來就可以了。

/fab-shadow-colors.png
/fab-shadow-colors.png

然後我們就可以在 Java 程式碼中,為 View 動態設定一個陰影效果。

/fab-shadow-javacode.png
/fab-shadow-javacode.png

這些引數,你可以自行根據效果配置,它們的含義,其實看看方法的簽名,你就清楚了,這裡就不再贅述了。

/shadowWrapper-method.png
/shadowWrapper-method.png

那麼,我們來看看使用 FAB 的 ShadowDrawableWrapper 模擬出來的陰影效果如何。

/fab-xiaoguo.png
/fab-xiaoguo.png

5.3 FAB 模擬陰影效果小結

前面提到,ShadowDrawableWrapper 的原理是對原本的 Drawable 做一個包裝,在外圍繪製陰影的效果,所以說它實際上,陰影部分也是需要佔據 View 的空間的,依然會有視覺上,View 會變小。

不過它的陰影顏色上可控的,也就是說我們可以動態的為其設定陰影的顏色,這樣應該會更靈活一些。

六、模擬 CardView 實現的陰影

我們知道,在 Android 對 Material Design 的效果中,有一些控制元件,就是自帶陰影效果的,並且它也是對低版本相容的。例如:FloatingActionButton 、CardView 等。

那麼,本小結就來看看 CardView 實現陰影的原理。

6.1 CardView 的陰影原理

CardView 在 support.design 包中,你是找不到的,它被放在了 cardview-v7 包中,現在已經可以單獨引用了。

CardView-v7 包中,程式碼非常的少。

/cardview-lib-path.png
/cardview-lib-path.png

一共就這麼幾個,一樣就可以看到來,有一些類是做 Api 版本相容的,並且也上如此。

/cardview-impl.png
/cardview-impl.png

在其中,還有一個 RoundRectDrawableWithShadow 類,它就是我們要找到,CardView 實現的 Drawable,它只在 CardViewJellybeanMr1 和 cardViewGingerbread 這兩個類中使用,CardViewApi21 中,依然是使用的 setElevation() 方法來處理的陰影。

用之前 FAB 的經驗,將 RoundRectDrawableWithShadow 直接拷貝出來,然後執行你會發現有報錯。主要是因為其中有個靜態的變數 sRoundRectHelper 為空了,沒有被初始化。

仔細查原始碼你會發現,它在 CardViewJellybeanMr1 和 CardViewGingerbread 的實現原理並不相同。它們會在 initStatic() 方法中,對 sRoundRectHelper 變數進行初始化。

CardViewJellybeanMr1.initStatic() 方法如下:

/cardview-jellybean-initstatic.png
/cardview-jellybean-initstatic.png

CardViewGingerbread.initStatic() 方法如下:

/cardview-bread-initstatic.png
/cardview-bread-initstatic.png

可以看到它們的實現方法,差異還是挺大的。

瞭解清楚這些,我們只需要 RoundRectDrawableWithShadow 的構造方法中,根據 Api Level 對他們進行不同的初始化即可,這些程式碼也上拷貝出來就可以直接用的。

繪製陰影的部分都大同小異,這裡就不詳細看了,有興趣的可以執行檢視原始碼,主要關注 drawShadow() 方法即可。

6.2 舉個 CardView 陰影的例子

首先,將 ShadowDrawableWrapper 完整的拷貝到我們的工程裡,並且在構造方法中,根據 Api Level ,用不同的邏輯初始化 sRoundRectHelper 。

還需要將 ShadowDrawableWrapper 使用到的幾個預設引數值也拷貝出來,當然我們已經有原始碼了,直接寫死也可以,我這裡選擇將它們原樣拷貝出來。

/cardview-dims.png
/cardview-dims.png

然後我們就可以在程式碼中,使用這個 RoundRectDrawableWithShadow 了。

/cardview-javacode.png
/cardview-javacode.png

最終,看看實現的陰影效果:

/cardview-xiaoguo.png
/cardview-xiaoguo.png

6.3 CardView 模擬陰影小結

CardView 模擬的陰影效果,在低版本上,也上會佔用 View 的原本的大小來繪製陰影,所以視覺上也會偏小。不過在高版本上,依然上使用 elevation來實現的,也就會造成在不同 Api Level 下,顯示的效果不一致的問題。

七、使用開源庫 ShadowLayout

最後再介紹一個開源庫,用一個 LayoutView 來實現陰影的效果。

Github 地址:

github.com/dmytrodanyl…

它完整的庫也只有一個類加一些屬性,整個專案結構如下。

/shadowlayout-project.png
/shadowlayout-project.png

並且提供了幾個屬性,用於配置陰影的效果。

/shadowlayout-res.png
/shadowlayout-res.png

使用起來也非常的方便,它上直接繼承自 FrameLayout 的,所以需要作為一個佈局來使用。

/shadowlayout-xml.png
/shadowlayout-xml.png

最後看看實現的效果。

/shadowlayout-xiaoguo.png
/shadowlayout-xiaoguo.png

它基本上可以實現一個類陰影的效果,不過應該是演算法的問題,導致陰影的邊緣太齊了,看著不真實,一般不推薦使用。

八、結語

介紹了這麼多在 Android 下實現陰影的效果,接下來給一張完整的效果圖吧,如果本文都看完了,我想你應該知道自己應該選擇那種方案了。

/WechatIMG30.jpeg
/WechatIMG30.jpeg

今天在承香墨影公眾號的後臺,回覆『成長』。我會送你一些我整理的學習資料,包含:Android反編譯、演算法、設計模式、Web專案原始碼。

推薦閱讀:

點贊或者分享吧~

相關文章