当你的设计师要求你在某个 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项目源码。
推荐阅读:
点赞或者分享吧~