当你的设计师要求你在某个 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 即可,你当然也可以选择一个图片或者一个纯色的

下面来看看 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:elevationandroid: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 的图,一般都是设计师会提供给我们。这里也推荐一个可以制作阴影效果的在线工具。

http://inloop.github.io/shadow4android/

通过这个工具,你可以对 .9 图做各种调整,例如:圆角、阴影的大小、阴影的颜色等等,都是非常方便的设置。前面例子中使用的 .9 文件,就是使用此工具制作的。

还有一种方式,就是使用

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 实现不同的逻辑。

可以看到,这里会根据 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 地址:

https://github.com/dmytrodanylyk/shadow-layout

它完整的库也只有一个类加一些属性,整个项目结构如下。

并且提供了几个属性,用于配置阴影的效果。

使用起来也非常的方便,它上直接继承自 FrameLayout 的,所以需要作为一个布局来使用。

最后看看实现的效果。

它基本上可以实现一个类阴影的效果,不过应该是算法的问题,导致阴影的边缘太齐了,看着不真实,一般不推荐使用。

八、结语

介绍了这么多在 Android 下实现阴影的效果,接下来给一张完整的效果图吧,如果本文都看完了,我想你应该知道自己应该选择那种方案了。

今天在承香墨影公众号的后台,回复『成长』。我会送你一些我整理的学习资料,包含:Android反编译、算法、设计模式、Web项目源码。

推荐阅读:

点赞或者分享吧~

聊聊 Material Design 里,阴影的那些事儿!的更多相关文章

  1. Material Design 设计--阴影的重要性

    <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_cont ...

  2. Material Design Lite,简洁惊艳的前端工具箱。

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,博客地址为http://www.cnblogs.com/jasonnode/ .网站上有对应每一 ...

  3. Android Material Design带UI变化

    谷歌Matias Duarte称,"Material Design是漂亮和大胆的.由于干净的排版和布局简单且easy理解.内容才是焦点. 谷歌I/O 014开发人员大会上宣布全新的设计语言& ...

  4. 【转】Material Design 折叠效果 Toolbar CollapsingToolbarLayout AppBarLayout

    我非常喜欢Material Design里折叠工具栏的效果,bilibili Android客户端视频详情页就是采用的这种设计.这篇文章的第二部分我们就通过简单的模仿bilibili视频详情页的实现来 ...

  5. 创建Material Design风格Android应用--自定义阴影和裁剪视图

    之前已经写过通过应用主题和使用ListView, CardView,应用Material Design样式,同一时候都都能够通过support library向下兼容.今天要写的阴影和视图裁剪.无法向 ...

  6. Material Design 概念,环境和基本属性

    Material Design 概念,环境和基本属性 Material Design是随Android 5.0推出的一种设计概念, 涉及到了跨平台和设备的视觉,动态,交互设计等方面.   设计概念 M ...

  7. Material Design学习笔记

    Wiki->移动开发->Android->Material Design-原质化设计 (友情链接:http://wiki.jikexueyuan.com/project/materi ...

  8. 【Android】进入Material Design时代

    由于本文引用了大量官方文档.图片资源,以及开源社区的Lib和相关图片资源,因此在转载的时候,务必注明来源,如果使用资源请注明资源的出处,尊重版权,尊重别人的劳动成果,谢谢! Material Desi ...

  9. Android Material Design简单使用

    吐槽 作为一个 Android developer,没有什么比拿着 UI 设计的一堆 iOS 风格的设计 来做需求更恶心的了,基本所有空间都要照着 iOS 来画一遍,Material Design 辣 ...

随机推荐

  1. 常用Linux命令、包括vi 、svn

    /etc/init.d/network restart//===========================================更新脚本cd /www/scripts更新站点./sta ...

  2. Python之面向对象与类

    本节内容 面向对象的概念 类的封装 类的继承 类的多态 静态方法.类方法 和 属性方法 类的特殊成员方法 子类属性查找顺序 一.面向对象的概念 1. "面向对象(OOP)"是什么? ...

  3. Maven 整合strut与Hibernate,获取不到Session

    struts使用的是2.3.24 Hibernate使用的5.0.7 注意hebernate一定要在struts之前申明,不然容易出现500错误, <project xmlns="ht ...

  4. oracle pl/sql 简介

    一.pl/sql 是什么pl/sql(procedural language/sql)是oracle在标准的sql语言上的扩展.pl/sql不仅允许嵌入sql语言,还可以定义变量和常量,允许使用条件语 ...

  5. DotNetCore跨平台~linux上还原自主nuget包需要注意的问题

    问题的产生的背景 由于我们使用了jenkins进行部署(jenkins~集群分发功能和职责处理),而对于.net core项目来说又是跨平台的,所以对它的项目拉取,包的还原,项目的编译和项目的发布都是 ...

  6. java集合系列——Map之HashMap介绍(八)

    1.HashMap的简介 (JDK1.7.0_79版本) HashMap是基于哈希表的Map实现的的,一个Key对应一个Value,允许使用null键和null值,不保证映射的顺序,特别是它不保证该顺 ...

  7. java集合系列——java集合概述(一)

    在JDK中集合是很重要的,学习java那么一定要好好的去了解一下集合的源码以及一些集合实现的思想! 一:集合的UML类图(网上下载的图片) Java集合工具包位置是java.util.* 二:集合工具 ...

  8. asp.net core封装layui组件示例分享

    用什么封装?自然是TagHelper啊,是啥?自己瞅文档去 在学习使用TagHelper的时候,最希望的就是能有个Demo能够让自己作为参考 怎么去封装一个组件? 不同的情况怎么去实现? 有没有更好更 ...

  9. ZOJ2105 终于找到错误

    ZOJ2105:点击打开链接 错误代码 #include<stdio.h> #include<stdlib.h> int q[110]; int main() { int a, ...

  10. 从头编写 asp.net core 2.0 web api 基础框架 (4) EF配置

    第1部分:http://www.cnblogs.com/cgzl/p/7637250.html 第2部分:http://www.cnblogs.com/cgzl/p/7640077.html 第3部分 ...