最近公司来了新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的宽高是100×100, 而UX要求暗影的offsetX, offestY为20dp, 我换算成elevation为多少dp后, 假设暗影占20×20的空间, 那终究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.
另一种暗影的思路
这一种思路, 其实便是彻底扔掉了ambient, spot shadow + elevation + outline provider
的系统, 走自己的暗影, 而且还蛮契合figma上的UX规划的.
这思路的首要思维便是运用:
paint.setShadowLayer(radius, offsetX, offsetY, shadowColor)
你能够把这个逻辑放到你的ViewGroup里, 或是放到一个Drawable里, 或是放到自己的自定义View里. 效果算还行.我个人感觉费事了点, 要额定多做些作业. 不过网上有不少三方库了, 要么笼统到Layout里, 要么笼统到Drawable里, 便利我们运用.
补白: Android8及以下的硬件加速都不支撑对view画暗影的Paint # setShadowLayer
的, 偏偏手机是默认打开硬件加速的, 所以这个方法也只能对Android P (Android 9.0)及以上的硬件加速才有效
p.s. 当然也能够用myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
来给View强制禁止硬件加速.
参考资料
- Material Design – light & shadow
- Material Design – Elevation
- Material Shadow on Android
- Material Design – Elevation & shadow
- 个人在公司项目中的实践经验
- Hardware Accelerator