當你的設計師要求你在某個 View 上增加陰影效果,那你只需要認真閱讀本文,陰影的問題就不再是問題。
一、前言
設計師的世界,與常人不同,有時候想要扁平化的風格,有時候又想要擬物化的風格。而在 Material Design 出來之后,為 UI 元素引入了高度的概念,它可以讓某個元素更為突出,顯示出它的重要性,更讓人有點擊的欲望。
在擬物化的設計里,UI 元素的高度,反應在效果上,就是在邊框上有陰影的效果,感覺它是距離底部有一個層次的關系。在 Material Design 的設計中,也大量的使用了 陰影 的效果,例如:FloatingActionButton、CardView 這些控件,都是默認支持陰影效果的。
如果你想了解 Material Design 中,更多關于陰影的設計,可以查閱官方文檔。
https://material.io/guidelines/material-design/elevation-shadows.html?hl=zh-cn
接下來,我們就來介紹一下,在 Android 的不同版本中,使用不同的方式,去實現陰影的效果。
先來看看實現的效果,雖然多,但是它們實現的方法都不相同。
二、陰影的效果
在擬物化的世界里,陰影主要是對三維空間中的 Z 屬性進行操作。下面是官網的介紹。
由 Z 屬性所表示的視圖高度將決定其陰影的視覺外觀:擁有較高 Z 值的視圖將投射更大且更柔和的陰影。 擁有較高 Z 值的視圖將擋住擁有較低 Z 值的視圖;不過視圖的 Z 值并不影響視圖的大小。
陰影是由提升的視圖的父項所繪制,因此將受到標準視圖裁剪的影響,而在默認情況下裁剪將由父項執行。
https://developer.android.com/training/material/shadows-clipping.html?hl=zh-cn#Elevation
靜態效果如下:
再加上,動態的效果應該更能讓你對陰影有所理解。
三、使用標準 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 即可,你當然也可以選擇一個圖片或者一個純色的 <shape/> 了。
下面來看看 elevation 屬性的效果:
往深里再看看 elevation 屬性的實現方式。
它最終還是調用的 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 屬性的設置效果:
使用 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 的話,那我們來看看它的原理是什么。
在 ViewCompat 中,會有很多個實現了 ViewCompatBaseImpl 的接口類,它們分別對應了不同的 Api Level ,會在靜態代碼塊中,根據當前運行設備的 Api Level ,做不同的實現。而這些,都是高版本繼承低版本的實現,來達到繼承兼容的效果。
ViewCompatBaseImpl 這個接口中,定義了很多關于 View 的操作 Api ,這些 Api 都是存在不同的 Api 版本限制的。
在 Api Level 21 中,本身就已經支持了這兩個屬性,也就不存在兼容性的問題了,所以它其中會直接調用 setElevation()
和 setTranslationZ()
方法。
那么,我們只需要關心 Api Level 21 以下的實現。通常來說,我們做兼容處理,一個方案就是在低版本上,使用一些只在低版本上存在 Api,來對高版本的效果進行模擬;另外一個方案就是放棄低版本,完全對它不做任何處理。
我們來看看 ViewCompat 是對 Elevation 是選用的那個方案。其實 Api Level 21 之下,都沒有對這兩個屬性的操作方法,做任何的處理,你一路追蹤下去可以追蹤到 ViewCompatBaseImpl 。
從這里可以看出,ViewCompat 沒有對這兩個方法做任何的兼容,在低版本上,沒有做任何的操作,這也導致了你如果使用 ViewCompat 的話,在低版本上是不會有陰影的效果的。沒有就是沒有,這里就不再單獨展示了。
那看看使用 ViewCompat 在高版本上的效果圖,其實和之前的也沒啥區別,不過擺在一起看更清晰一些。
3.4 標準 Api 小結
到現在你也能看到,如果不在意 Api level 的話,你完全可以使用 android:elevation
和 android:translationZ
兩個屬性來做的陰影的效果,效果也是非常好的,而且它的陰影實際上是不占用 View 的布局大小的,它會在原本的布局之外,向外擴散,所以也不會影響 View 本身大小的視覺效果。
不過它也有缺陷,你只能通過設定這兩個屬性來調整陰影的大小,沒辦法做到精確掌控,并且無法修改陰影的顏色。
最新的 Android 版本市場占有率,你可以在這個網站上查到。
https://developer.android.com/about/dashboards/index.html?hl=zh-cn
截止到本文編寫的時候,低于 5.0 的版本,差不多在 20% 左右,是否對這部分用戶,放棄陰影的效果,取決于你的產品和設計師。
如果你需要兼容低版本的設備,后面介紹的一些方法,都可以做到,繼續往下閱讀吧。
四、使用9Patch圖
4.1 什么上 9Patch 圖
如果你需要兼容低版本的 Android 設備,使用 android:elevation 和 android:translationZ 是無法做到的,它們會在低版本上失效,完全沒有效果,當然前提是你需要做好 Warning 的處理。
而這種陰影的效果,使用 .9
圖,也是一個不錯的選擇。
.9
圖 就是 9Patch, 引用官網的介紹:
Draw 9-patch 工具是 Android Studio 中包含的一種 WYSIWYG(所見即所得)編輯器,利用此工具,您可以創建能夠自動調整大小以適應視圖內容和屏幕尺寸的位圖圖像。圖像的選定部分可以根據圖像內繪制的指示器在水平或豎直方向上調整比例。
https://developer.android.com/studio/write/draw9patch.html?hl=zh-cn
4.2 使用 9Patch 設置陰影
直接制作一個帶陰影效果的 .9
圖片,然后設置好內容區域和拉伸區域,就可以在其中模擬出陰影的效果。
舉個例子,使用一個 .9
圖,然后設置在 ImageView 上的背景。
在 layout-xml 上,只需要給 ImageView 設置好 android:background
就可以了。
來看看它實現的效果:
使用 .9
圖設置的陰影,效果一般都是有保障的。不過它會作為 View 的背景被設置,所以陰影上占據 View 的大小的,所以使用圖片模擬出來的陰影,View 本身的視覺效果會小。
放張單圖,可能看不出效果,將一個使用 ViewCompat 實現的效果,放在一起,你就可以看到對比的效果。
這里,兩個 ImageView ,實際設置的大小,都是 100dp,但是視覺上,使用 .9 實現的效果,視覺效果就會小。
4.3 快速制作 9Patch
.9
的圖,一般都是設計師會提供給我們。這里也推薦一個可以制作陰影效果的在線工具。
通過這個工具,你可以對 .9
圖做各種調整,例如:圓角、陰影的大小、陰影的顏色等等,都是非常方便的設置。前面例子中使用的 .9
文件,就是使用此工具制作的。
還有一種方式,就是使用 <layer-list> 這個層級的 Drawable 去模擬陰影,等于一層一層的疊加。不過使用這種方式太麻煩了,而且效果也很難做到非常的好,一般也不推薦。
4.4 9Patch 模擬陰影小結
使用 .9 圖,制作陰影,基本上不需要擔心效果的問題,使用起來也非常的方便。唯一的問題就是它的陰影部分,會占用 View 本身的大小,導致 View 在視覺上縮小。
總結來說,它的優點:
- 實現方便,只需要設置背景即可。
- 陰影的效果可控,顏色、圓角、陰影大小都是可以調整的。
它的缺點也非常的明顯:
- 為了讓 View 在視覺上和效果圖匹配,需要預留出陰影的空間。
五、使用 FAB 的原理模擬陰影
我們知道,在 Android 對 Material Design 的效果中,有一些控件,就是自帶陰影效果的,并且它也是對低版本兼容的。例如:FloatingActionButton 、CardView 等。
那么,本小結就來看看 FloatingActionButton 實現陰影的原理。
5.1 FAB 的陰影原理
就 FAB 這種有 Support.design 包支持的控件,一般都有對 不同的 Api Level 做支持處理,在 FAB 之中也是一樣的,它會根據不同的 Api Level 實現不同的邏輯。
可以看到,這里會根據 21、14、<14 三個條件,分別使用不同的實現類,它們內部實際上實現的都是相同的功能。
如果仔細觀察這些 FAB 不同版本的實現類的源碼,你可以發現它的陰影效果,都是基于一個 ShadowDrawableWrapper 這個 Drawable 來實現的。
例如在 FloatingActionButtonGingerbread 中,就有這樣一段設置背景的代碼。
這里完全上依賴 ShadowDrawableWrapper 來做的陰影效果。
不過 ShadowDrawableWrapper 被聲明的可見性為包內可見,所以我們沒有辦法直接使用它。
不過,鑒于 support.design 包中的類,一般都是為了兼容做處理,這里我們只需要將它和它實現的接口 DrawableWrapper 這兩個類,拷貝出來,就可以直接使用了。它們的源碼都在 android.support.design/widget
包下面,非常容易找到。
它的原理是在你本文需要設置的 Drawable 之外,再包裝一個 Drawable ,然后在這個包裝的 Drawable 上繪制陰影。
繪制的代碼挺多的,這里就不貼代碼了,有興趣可以看看它的源碼,主要關注 drawShadow()
方法即可。
而如果你在拷貝源碼的時候,應該能發現,它實際上是可以支持改變陰影的顏色的,如果你有這種需求,只需要再擴展它的構造方法,或者直接在 colors.xml 中配置對應的顏色,它設置顏色地方如下。
可以看到,它主要用三個顏色來做一個漸變的陰影效果。
5.2 使用 FAB 的原理模擬陰影效果
前面說的,我們只需要將 ShadowDrawableWrapper 和 DrawableWrapper 這兩個文件復制到我們的工程內,稍微修改一下它們的依賴關系。
如果直接拷貝源碼,你會發現它還依賴三個顏色,分別是用于設置陰影的顏色的,這個前面也提到過。一般而言,我們不需要設置它,直接從源碼中將它們拷貝出來就可以了。
然后我們就可以在 Java 代碼中,為 View 動態設置一個陰影效果。
這些參數,你可以自行根據效果配置,它們的含義,其實看看方法的簽名,你就清楚了,這里就不再贅述了。
那么,我們來看看使用 FAB 的 ShadowDrawableWrapper 模擬出來的陰影效果如何。
5.3 FAB 模擬陰影效果小結
前面提到,ShadowDrawableWrapper 的原理是對原本的 Drawable 做一個包裝,在外圍繪制陰影的效果,所以說它實際上,陰影部分也是需要占據 View 的空間的,依然會有視覺上,View 會變小。
不過它的陰影顏色上可控的,也就是說我們可以動態的為其設置陰影的顏色,這樣應該會更靈活一些。
六、模擬 CardView 實現的陰影
我們知道,在 Android 對 Material Design 的效果中,有一些控件,就是自帶陰影效果的,并且它也是對低版本兼容的。例如:FloatingActionButton 、CardView 等。
那么,本小結就來看看 CardView 實現陰影的原理。
6.1 CardView 的陰影原理
CardView 在 support.design 包中,你是找不到的,它被放在了 cardview-v7 包中,現在已經可以單獨引用了。
CardView-v7 包中,代碼非常的少。
一共就這么幾個,一樣就可以看到來,有一些類是做 Api 版本兼容的,并且也上如此。
在其中,還有一個 RoundRectDrawableWithShadow 類,它就是我們要找到,CardView 實現的 Drawable,它只在 CardViewJellybeanMr1 和 cardViewGingerbread 這兩個類中使用,CardViewApi21 中,依然是使用的 setElevation()
方法來處理的陰影。
用之前 FAB 的經驗,將 RoundRectDrawableWithShadow 直接拷貝出來,然后運行你會發現有報錯。主要是因為其中有個靜態的變量 sRoundRectHelper 為空了,沒有被初始化。
仔細查源碼你會發現,它在 CardViewJellybeanMr1 和 CardViewGingerbread 的實現原理并不相同。它們會在 initStatic()
方法中,對 sRoundRectHelper 變量進行初始化。
CardViewJellybeanMr1.initStatic() 方法如下:
CardViewGingerbread.initStatic() 方法如下:
可以看到它們的實現方法,差異還是挺大的。
了解清楚這些,我們只需要 RoundRectDrawableWithShadow 的構造方法中,根據 Api Level 對他們進行不同的初始化即可,這些代碼也上拷貝出來就可以直接用的。
繪制陰影的部分都大同小異,這里就不詳細看了,有興趣的可以執行查看源碼,主要關注 drawShadow() 方法即可。
6.2 舉個 CardView 陰影的例子
首先,將 ShadowDrawableWrapper 完整的拷貝到我們的工程里,并且在構造方法中,根據 Api Level ,用不同的邏輯初始化 sRoundRectHelper 。
還需要將 ShadowDrawableWrapper 使用到的幾個默認參數值也拷貝出來,當然我們已經有源碼了,直接寫死也可以,我這里選擇將它們原樣拷貝出來。
然后我們就可以在代碼中,使用這個 RoundRectDrawableWithShadow 了。
最終,看看實現的陰影效果:
6.3 CardView 模擬陰影小結
CardView 模擬的陰影效果,在低版本上,也上會占用 View 的原本的大小來繪制陰影,所以視覺上也會偏小。不過在高版本上,依然上使用 elevation來實現的,也就會造成在不同 Api Level 下,顯示的效果不一致的問題。
七、使用開源庫 ShadowLayout
最后再介紹一個開源庫,用一個 LayoutView 來實現陰影的效果。
Github 地址:
它完整的庫也只有一個類加一些屬性,整個項目結構如下。
并且提供了幾個屬性,用于配置陰影的效果。
使用起來也非常的方便,它上直接繼承自 FrameLayout 的,所以需要作為一個布局來使用。
最后看看實現的效果。
它基本上可以實現一個類陰影的效果,不過應該是算法的問題,導致陰影的邊緣太齊了,看著不真實,一般不推薦使用。
八、結語
介紹了這么多在 Android 下實現陰影的效果,接下來給一張完整的效果圖吧,如果本文都看完了,我想你應該知道自己應該選擇那種方案了。
今天在承香墨影公眾號的后臺,回復『成長』。我會送你一些我整理的學習資料,包含:Android反編譯、算法、設計模式、Web項目源碼。
推薦閱讀:
點贊或者分享吧~