最近公司来了新UX总监, 很喜欢给规划添加浓重的, 而且是好几层的暗影. 这下就苦了咱们Android开发了. 由于是Android不支撑啊, 巧妇也难为无米之炊啊. (折中方法也不是没有, 便是自己把暗影做个view, 但它的blur这些比较费事, 做过Android的都知道这个Blur要用到BlurScript之类, 做起来不容易)

Android的shadow之痛

以下图中一个矩形有暗影为例, 它的shadow是有多种参数的, 首要便是: offsetX, offsetY, blur, spread, color & alpha. Ux在figma等软件上规划好的姿态如下:

坑爹的shadow -- 总结 与 各种坑

而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若是离手机屏幕有一些高度的话, 那就会在手机屏幕上构成暗影.

坑爹的shadow -- 总结 与 各种坑

留意这个暗影比较传神, 在边缘由于有光照与暗影的同时干涉, 所以暗影较浅 (即下图中的赤色部分) 而中心的暗影更浓 (即下图的蓝色部分)

坑爹的shadow -- 总结 与 各种坑

  • 较浅暗影在Material Design中的术语叫做: ambient shadow (环境暗影)
  • 较深暗影的术语叫: spot shaodw (聚光灯暗影)

相同, 上面也说了, 若是你的View紧贴手机屏幕, 那也不会有暗影的. 你的View只需抬起来一点高度, 才会构成暗影. 这跟日常日子中的体会是一样的. 而这个”抬起来的高度”, 在Android中的术语便是: elevation, 你能够理解为z轴上的高度啦. 当elevation不同, 天然暗影也不一样. 如下面的表格, 分别代表了elevation为2dp与10dp时的结果:

坑爹的shadow -- 总结 与 各种坑

好了, 上面便是重要的三个暗影要害: ambient shadow, spot shadow, 以及elevation.

暗影的实践

当你的view有了elevation时, 你就天然会构成两种暗影: ambient与spot shadow.

坑爹的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上下载这个布景,

坑爹的shadow -- 总结 与 各种坑

并导入到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. 其它的可选值如下:

坑爹的shadow -- 总结 与 各种坑

这下其实也就解说了, 为什么要有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)及以上的硬件加速才有效

坑爹的shadow -- 总结 与 各种坑

p.s. 当然也能够用myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null)来给View强制禁止硬件加速.

参考资料

  1. Material Design – light & shadow
  2. Material Design – Elevation
  3. Material Shadow on Android
  4. Material Design – Elevation & shadow
  5. 个人在公司项目中的实践经验
  6. Hardware Accelerator