當你的設計師要求你在某個 View 上增長陰影效果,那你只須要認真閱讀本文,陰影的問題就再也不是問題。html
設計師的世界,與常人不一樣,有時候想要扁平化的風格,有時候又想要擬物化的風格。而在 Material Design 出來以後,爲 UI 元素引入了高度的概念,它可讓某個元素更爲突出,顯示出它的重要性,更讓人有點擊的慾望。java
在擬物化的設計裏,UI 元素的高度,反應在效果上,就是在邊框上有陰影的效果,感受它是距離底部有一個層次的關係。在 Material Design 的設計中,也大量的使用了 陰影 的效果,例如:FloatingActionButton、CardView 這些控件,都是默認支持陰影效果的。android
若是你想了解 Material Design 中,更多關於陰影的設計,能夠查閱官方文檔。git
material.io/guidelines/…github
接下來,咱們就來介紹一下,在 Android 的不一樣版本中,使用不一樣的方式,去實現陰影的效果。web
先來看看實現的效果,雖然多,可是它們實現的方法都不相同。算法
在擬物化的世界裏,陰影主要是對三維空間中的 Z 屬性進行操做。下面是官網的介紹。設計模式
由 Z 屬性所表示的視圖高度將決定其陰影的視覺外觀:擁有較高 Z 值的視圖將投射更大且更柔和的陰影。 擁有較高 Z 值的視圖將擋住擁有較低 Z 值的視圖;不過視圖的 Z 值並不影響視圖的大小。api
陰影是由提高的視圖的父項所繪製,所以將受到標準視圖裁剪的影響,而在默認狀況下裁剪將由父項執行。app
靜態效果以下:
再加上,動態的效果應該更能讓你對陰影有所理解。
Material Design 首次出如今 Android 5.0 中,以後又有一些 Support 包,讓更低的版本,對 Material Design 進行支持。
而在 Api Level 21 之中,增長了兩個屬性 :
這兩個屬性,有對應的 xml 屬性和 setXxx()
方法,而 Z 軸的改變,主要是由這兩個屬性決定的。
Z = elevation + translationZ
因此,若是你的 App 的 minSdkVersion 就是 21 的話,直接使用這兩個屬性是最優的解決辦法。
elevation 屬性,主要用於給 View 增長一個高度,能夠直接被加在 View 控件上,呈如今界面上,就是一個帶陰影的效果。
在 layout-xml 佈局中,能夠經過 android:elevation
屬性來設置,而在 Java 代碼中,經過 View.setElevation()
方法來使用它。
直接使用 elevation 屬性設置便可,它接收一個高度的參數,只須要按咱們的須要配置便可。
須要注意的是,View 的陰影必定是須要有背景的 View 在視覺上增高以後,投射出來的。也就是相似於打光的陰影效果。簡單來講,就是須要爲 View 設置一個 Background,可使用 android:background
屬性或者 View.setBackground()
方法設置,否者 elevation 的屬性設置將無效。這裏的 Background 只須要設置一個 Drawable 便可,你固然也能夠選擇一個圖片或者一個純色的 了。
下面來看看 elevation 屬性的效果:
往深裏再看看 elevation 屬性的實現方式。
它最終仍是調用的 mRenderNode 去作的操做,在追蹤下去,就會發現它底層是用的 native 的方法實現的,因此應該不是咱們所理解的用 2D 的漸變模擬陰影的效果。
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 去作的實現。
前面就已經提到,當你的 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 在高版本上的效果圖,其實和以前的也沒啥區別,不過擺在一塊兒看更清晰一些。
到如今你也能看到,若是不在乎 Api level 的話,你徹底可使用 android:elevation
和 android:translationZ
兩個屬性來作的陰影的效果,效果也是很是好的,並且它的陰影其實是不佔用 View 的佈局大小的,它會在本來的佈局以外,向外擴散,因此也不會影響 View 自己大小的視覺效果。
不過它也有缺陷,你只能經過設定這兩個屬性來調整陰影的大小,沒辦法作到精確掌控,而且沒法修改陰影的顏色。
最新的 Android 版本市場佔有率,你能夠在這個網站上查到。
截止到本文編寫的時候,低於 5.0 的版本,差很少在 20% 左右,是否對這部分用戶,放棄陰影的效果,取決於你的產品和設計師。
若是你須要兼容低版本的設備,後面介紹的一些方法,均可以作到,繼續往下閱讀吧。
若是你須要兼容低版本的 Android 設備,使用 android:elevation 和 android:translationZ 是沒法作到的,它們會在低版本上失效,徹底沒有效果,固然前提是你須要作好 Warning 的處理。
而這種陰影的效果,使用 .9
圖,也是一個不錯的選擇。
.9
圖 就是 9Patch, 引用官網的介紹:
Draw 9-patch 工具是 Android Studio 中包含的一種 WYSIWYG(所見即所得)編輯器,利用此工具,您能夠建立可以自動調整大小以適應視圖內容和屏幕尺寸的位圖圖像。圖像的選定部分能夠根據圖像內繪製的指示器在水平或豎直方向上調整比例。
直接製做一個帶陰影效果的 .9
圖片,而後設置好內容區域和拉伸區域,就能夠在其中模擬出陰影的效果。
舉個例子,使用一個 .9
圖,而後設置在 ImageView 上的背景。
在 layout-xml 上,只須要給 ImageView 設置好 android:background
就能夠了。
來看看它實現的效果:
使用 .9
圖設置的陰影,效果通常都是有保障的。不過它會做爲 View 的背景被設置,因此陰影上佔據 View 的大小的,因此使用圖片模擬出來的陰影,View 自己的視覺效果會小。
放張單圖,可能看不出效果,將一個使用 ViewCompat 實現的效果,放在一塊兒,你就能夠看到對比的效果。
這裏,兩個 ImageView ,實際設置的大小,都是 100dp,可是視覺上,使用 .9 實現的效果,視覺效果就會小。
.9
的圖,通常都是設計師會提供給咱們。這裏也推薦一個能夠製做陰影效果的在線工具。
經過這個工具,你能夠對 .9
圖作各類調整,例如:圓角、陰影的大小、陰影的顏色等等,都是很是方便的設置。前面例子中使用的 .9
文件,就是使用此工具製做的。
還有一種方式,就是使用 這個層級的 Drawable 去模擬陰影,等於一層一層的疊加。不過使用這種方式太麻煩了,並且效果也很難作到很是的好,通常也不推薦。
使用 .9 圖,製做陰影,基本上不須要擔憂效果的問題,使用起來也很是的方便。惟一的問題就是它的陰影部分,會佔用 View 自己的大小,致使 View 在視覺上縮小。
總結來講,它的優勢:
它的缺點也很是的明顯:
咱們知道,在 Android 對 Material Design 的效果中,有一些控件,就是自帶陰影效果的,而且它也是對低版本兼容的。例如:FloatingActionButton 、CardView 等。
那麼,本小結就來看看 FloatingActionButton 實現陰影的原理。
就 FAB 這種有 Support.design 包支持的控件,通常都有對 不一樣的 Api Level 作支持處理,在 FAB 之中也是同樣的,它會根據不一樣的 Api Level 實現不一樣的邏輯。
能夠看到,這裏會根據 2一、1四、<14 三個條件,分別使用不一樣的實現類,它們內部實際上實現的都是相同的功能。
若是仔細觀察這些 FAB 不一樣版本的實現類的源碼,你能夠發現它的陰影效果,都是基於一個 ShadowDrawableWrapper 這個 Drawable 來實現的。
例如在 FloatingActionButtonGingerbread 中,就有這樣一段設置背景的代碼。
這裏徹底上依賴 ShadowDrawableWrapper 來作的陰影效果。
不過 ShadowDrawableWrapper 被聲明的可見性爲包內可見,因此咱們沒有辦法直接使用它。
不過,鑑於 support.design 包中的類,通常都是爲了兼容作處理,這裏咱們只須要將它和它實現的接口 DrawableWrapper 這兩個類,拷貝出來,就能夠直接使用了。它們的源碼都在 android.support.design/widget
包下面,很是容易找到。
它的原理是在你本文須要設置的 Drawable 以外,再包裝一個 Drawable ,而後在這個包裝的 Drawable 上繪製陰影。
繪製的代碼挺多的,這裏就不貼代碼了,有興趣能夠看看它的源碼,主要關注 drawShadow()
方法便可。
而若是你在拷貝源碼的時候,應該能發現,它其實是能夠支持改變陰影的顏色的,若是你有這種需求,只須要再擴展它的構造方法,或者直接在 colors.xml 中配置對應的顏色,它設置顏色地方以下。
能夠看到,它主要用三個顏色來作一個漸變的陰影效果。
前面說的,咱們只須要將 ShadowDrawableWrapper 和 DrawableWrapper 這兩個文件複製到咱們的工程內,稍微修改一下它們的依賴關係。
若是直接拷貝源碼,你會發現它還依賴三個顏色,分別是用於設置陰影的顏色的,這個前面也提到過。通常而言,咱們不須要設置它,直接從源碼中將它們拷貝出來就能夠了。
而後咱們就能夠在 Java 代碼中,爲 View 動態設置一個陰影效果。
這些參數,你能夠自行根據效果配置,它們的含義,其實看看方法的簽名,你就清楚了,這裏就再也不贅述了。
那麼,咱們來看看使用 FAB 的 ShadowDrawableWrapper 模擬出來的陰影效果如何。
前面提到,ShadowDrawableWrapper 的原理是對本來的 Drawable 作一個包裝,在外圍繪製陰影的效果,因此說它實際上,陰影部分也是須要佔據 View 的空間的,依然會有視覺上,View 會變小。
不過它的陰影顏色上可控的,也就是說咱們能夠動態的爲其設置陰影的顏色,這樣應該會更靈活一些。
咱們知道,在 Android 對 Material Design 的效果中,有一些控件,就是自帶陰影效果的,而且它也是對低版本兼容的。例如:FloatingActionButton 、CardView 等。
那麼,本小結就來看看 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() 方法便可。
首先,將 ShadowDrawableWrapper 完整的拷貝到咱們的工程裏,而且在構造方法中,根據 Api Level ,用不一樣的邏輯初始化 sRoundRectHelper 。
還須要將 ShadowDrawableWrapper 使用到的幾個默認參數值也拷貝出來,固然咱們已經有源碼了,直接寫死也能夠,我這裏選擇將它們原樣拷貝出來。
而後咱們就能夠在代碼中,使用這個 RoundRectDrawableWithShadow 了。
最終,看看實現的陰影效果:
CardView 模擬的陰影效果,在低版本上,也上會佔用 View 的本來的大小來繪製陰影,因此視覺上也會偏小。不過在高版本上,依然上使用 elevation來實現的,也就會形成在不一樣 Api Level 下,顯示的效果不一致的問題。
最後再介紹一個開源庫,用一個 LayoutView 來實現陰影的效果。
Github 地址:
它完整的庫也只有一個類加一些屬性,整個項目結構以下。
而且提供了幾個屬性,用於配置陰影的效果。
使用起來也很是的方便,它上直接繼承自 FrameLayout 的,因此須要做爲一個佈局來使用。
最後看看實現的效果。
它基本上能夠實現一個類陰影的效果,不過應該是算法的問題,致使陰影的邊緣太齊了,看着不真實,通常不推薦使用。
介紹了這麼多在 Android 下實現陰影的效果,接下來給一張完整的效果圖吧,若是本文都看完了,我想你應該知道本身應該選擇那種方案了。
今天在承香墨影公衆號的後臺,回覆『成長』。我會送你一些我整理的學習資料,包含:Android反編譯、算法、設計模式、Web項目源碼。
推薦閱讀:
點贊或者分享吧~