作者:snwrking
最近公司来了新UX总监, 很喜欢给设计添加浓重的, 而且是好几层的阴影. 这下就苦了我们Android开发了. 因为是Android不支持啊, 巧妇也难为无米之炊啊. (折中方法也不是没有, 就是自己把阴影做个view, 但它的blur这些比较麻烦, 做过Android的都知道这个Blur要用到BlurScript之类, 做起来不容易)
Android的shadow之痛
以下图中一个矩形有阴影为例, 它的shadow是有多种参数的, 主要就是: offsetX, offsetY, blur, spread, color & alpha. Ux在figma等软件上设计好的样子如下:
而UX与PO天天挂嘴边的: “为什么人家iOS可以, 为什么人家web可以, 就你Android不行?”, 这个是因为人家有支持啊.
- web用css:
filter: drop-shadow(5px 5px 5px rgba(0,0,0,0.3));
- iOS的view的layer也支持 (iOS的绘制也是有一层一层的, 类似Android中的FrameLayout一样, 可以一层层堆叠):
let yourView = UIView()
yourView.layer.shadowColor = UIColor.black.cgColor
yourView.layer.shadowOpacity = 1
yourView.layer.shadowOffset = .zero
yourView.layer.shadowRadius = 10
但我们Android确实对shadow的支持从来就弱. 若是对文字的阴影, 那TextView确实有部分支持, 但blur效果就不支持:
<TextView android:id="@+id/txt_example1"
...
android:shadowColor="@color/text_shadow"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="2"
要是想像UX要求的一样, 给任意View添加阴影, 好就麻烦了. 当然, Android自己也意识到这一点了, 所以在Android 5之后, 即在新引入的Material Design里添加了阴影的支持. 但它的阴影理论自成一套, 根本跟figma上的shadowOffsetX, shadowOffsetY, shadowBlur, shadowRadius不一样. 它的一套理论更像是光照系统.
Android 5.0之后的阴影&光照系统 – 理论部分
题外话: 现在app的minSDK不至于少于5.0吧, 所以现在只郑重讲Android5.0之后的阴影系统.
这一章节是阴影的理论部分. 话说我也不想讲理论, 太枯燥, 所以我尽量讲得简略些, 只挑重点讲. 后面再结合实践来验证这些理论, 来加深理解.
Material Design其实更像是一个光照系统. 它假定在远方有一个光源, 然后照向你的view. 这样你的view若是离手机屏幕有一些高度的话, 那就会在手机屏幕上形成阴影.
注意这个阴影比较逼真, 在边缘因为有光照与阴影的同时干涉, 所以阴影较浅 (即下图中的红色部分) 而中间的阴影更浓 (即下图的蓝色部分)
- 较浅阴影在Material Design中的术语叫做: ambient shadow (环境阴影)
- 较深阴影的术语叫: spot shaodw (聚光灯阴影)
同样, 上面也说了, 若是你的View紧贴手机屏幕, 那也不会有阴影的. 你的View只有抬起来一点高度, 才会形成阴影. 这跟日常生活中的体验是一样的. 而这个"抬起来的高度", 在Android中的术语就是: elevation, 你可以理解为z轴上的高度啦. 当elevation不同, 自然阴影也不一样. 如下面的表格, 分别代表了elevation为2dp与10dp时的结果:
好了, 上面就是重要的三个阴影关键: ambient shadow
, spot shadow
, 以及elevation
.
阴影的实践
当你的view有了elevation时, 你就天然会形成两种阴影: ambient与spot shadow.
同样Android也提供了一共5个API来帮我们设置阴影. 我按照since API Level xx
做了分类:
- Since Api 21:
android:elevation
: 在view中设定android:spotShadowAlpha
: 在theme这个xml中设定android:ambientShadowAlpha
: 在theme这个xml中设定
- Since Api 28:
android:spotShadowColor
: 在view中设定android:ambientShadowColor
: 在view中设定
-
听起来好像蛮简单的, 有了这5个api, 微调下值就能得到和UX设计的近似的阴影, 这就算完工了. 但现实生活中开发总是悲催得多, 比如说你设了这5个api, 但现实中却发现一点点子阴影都没有. 这是怎么了?
- 这就不得不说官网上根本没有详细讲述的一些坑了. 不解决这些坑, 我们的阴影仍是不行的.
设置阴影的多个坑
坑1: 设置了elevation仍没有阴影
下面的代码就是我以前写过的一个代码. 按理说我的elevation已经有了, 而ambient + spot shadow的color, alpha都有默认值 (手机上就淡淡的黑色灰影; TV上则是更重些的灰色), 那就应该有阴影. 但不幸的是, 最终效果是完全没有阴影效果.
<SomeView
android:elevation="24dp"
/>
原因是: Android中你的View得有一个背景, 颜色或图片都可以, 那你才会有阴影. 当我上面的view没有背景, 那Android根本就不会为它生成背景, 因为它把这个view当成透明的了, 一个透明的东西在光照下自然是没有阴影的.
解决办法:
<SomeView
android:background="@drawable/some_bg"
android:elevation="24dp"
/>
坑2: 下载的图片素材做bg, 但仍没有阴影
我有一个view, 其是有背景的. 我去figma上下载这个背景,
并导入到Android Studio后, 命名为bg_pink_polygon
, 但下面的代码仍是没有阴影.
<SomeView
android:background="@drawable/bg_pink_polygon"
android:elevation="24dp"
/>
: 原因其实是Android要求你的view有bg, 才可能会有阴影 但前提是这个bg, 不能是SVG形成的<vector>
xml, 不然Android也不会为它生成阴影.
也就是说, 你的bg可以是这样的:
- 纯color值, 如
#ff00cc
- 纯png, jpg图片, 如
bg_abc.png
- 或为点9图片, 如
bg_abc.9.png
- 或是
<shape>
的drawable xml, - 或是item为
<shape>
的<layer-list>
的drawable xml
但是, 唯独有一点, 你的bg不能是<vector>
的drawable, 不然就没有阴影.
坑3: 阴影需要额外空间吗?
比如说我们的View的宽高是100x100, 而UX要求阴影的offsetX, offestY为20dp, 我换算成elevation为多少dp后, 假设阴影占20x20的空间, 那最终UI效果要这样吗?
<FrameLayout width=120dp height=120dp>
<View width=100dp height=100dp/>
</FrameLayout>
: 一般来说不需要. 这一点Android做得还是可以的, 你只要考虑你的View的尺寸就行了, 阴影的空间Android会自动画出来, 不用你担心.
但是现实生活中更复杂些, 比如要求一个view有阴影, 而这个view在另外的其它自定义View中, 那这时就不好讲. 可能这时的阴影就被cut off了. 我就碰到过这样的实例.
至于原因则可能是为android中默认是child view不能超过parent view group的边界, 超出了的部分会不被绘制出来. 这也包括了阴影. 所以阴影也被截断了.
这里你可能就要做额外一层layout. (这一点蛮恶心了, 牺牲了性能就为了个阴影, 所以我也不喜欢过多的阴影设计)
<!-- 原来是 -->
<ConstraintLayout elevation=24dp backgorund=xx>
.... <!-- children -->
现在为了给这个constraint layout添加阴影, 并不被截断, 就得外加一层layout
<FrameLayout padding=8dp clipToPadding=false>
<ConstraintLayout elevation=24dp backgorund=xx>
这个FrameLayout存在的唯一目的就是留出空间, 来给constraitLayout提供绘制阴影的空间.
更多阴影设置
前言: 为什么view一定要有bg, 才会有阴影的可能?
其实我们上面的话不太对, 即这句: “光源对view照下来, 形成了阴影”
而上一节中, 我们修正了这句话, 即应该是: “光源对view的背景照下来, 形成了阴影”.
其实这次的修正仍是不对的, 正确的说法是"光源对View的outline provider照下来, 形成了阴影".
Android中View是自带了outline provider的, 默认值就是background. 其它的可选值如下:
这下其实也就解释了, 为什么要有bg, 才会有shadow : 因为默认就是background的outline provider啊. 要是view没有background, 就没了outline provider, 那就自然就没了阴影.
outline provider
那是不是说当我把outline provider设置了非background的其它值, 那即使这个view没有bg, 只要有elevation就会有阴影? : 是的, 答对了.
自定义shadow的形状
outline provider的另一作用, 就是可以让你自定义shadow的shape, 比如说不再是矩形, 可以变成圆形, 五角星形, …
你所需要做的, 就是自定义一个outline provider
class MyShadowOutlineProvider(
val cornerRadius: Float = 0f,
var offsetX: Int = 0
var offsetY: Int = 0
) : ViewOutlineProvider() {
private val rect: Rect = Rect()
override fun getOutline(view: View?, outline: Outline?) {
view?.background?.copyBounds(rect)
rect.offset(offsetX, offsetY)
outline?.setRoundRect(rect, cornerRadius)
}
}
然后view中指明使用这个outline provider:
outlineProvider = MyShadowOutlineProvider(14f.dpToPx(), 1f, 1f, 0)
btnShadowDemo.outlineProvider = outlineProvider
btnShadowDemo.elevation = 30f //elevation仍是需要的!
最佳实践推荐
theme中设置alpha为1
因为theme中把ambientShadowAlpha, spotShadowAlpha给定死了. "定死了"就是无法在代码中修改这两个的alpha值, 就不太灵活.
一个取巧的办法则是:
<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:ambientShadowAlpha">1</item>
<item name="android:spotShadowAlpha">1</item>
... ...
</style>
1). theme中把这两个alpha全设为1 2). 然后阴影color就可以加上alpha了, 如
// 原来是:
setSpotShadowColor(0x000000) //颜色是 rgb
// 现在则要给color加上alpha
setSpotShadowColor(0x88000000) //变成了 argb
这样一来我们就能灵活更改shadow的颜色与透明度了.
若无定制shadow形状的要求
那就是:
1). 给view添加非svg的bg
2). 再加上elevation
3). (可选) 可修改ambient/spot shadow的color
阴影就出来了
若有定制shadow形式的要求
那就要:
1). 自定义一个outline provider
2). view.outlineProvider = myOutlineProvider
3). 再加上elevation
阴影也同样出来了, 还是你自己定制的shape.
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap