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

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

作者:snwrking 链接:https://juejin.cn/post/7259007674521124924

Android的shadow之痛

以下图中一个矩形有阴影为例, 它的shadow是有多种参数的, 主要就是: offsetX, offsetY, blur, spread, color & alpha.  Ux在figma等软件上设计好的样子如下:

0567de71d1532bb32a36e445dfcfcbbc.jpeg

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

注意这个阴影比较逼真, 在边缘因为有光照与阴影的同时干涉, 所以阴影较浅 (即下图中的红色部分) 而中间的阴影更浓 (即下图的蓝色部分)

e21074b0126ccd2260381f1aea5524e5.jpeg

  • 较浅阴影在Material Design中的术语叫做: ambient shadow (环境阴影)

  • 较深阴影的术语叫: spot shaodw (聚光灯阴影)

同样, 上面也说了, 若是你的View紧贴手机屏幕, 那也不会有阴影的. 你的View只有抬起来一点高度, 才会形成阴影. 这跟日常生活中的体验是一样的. 而这个"抬起来的高度", 在Android中的术语就是: elevation, 你可以理解为z轴上的高度啦. 当elevation不同, 自然阴影也不一样. 如下面的表格, 分别代表了elevation为2dp与10dp时的结果:

96d3e59256243023fccc640ae8a6bd59.jpeg

好了, 上面就是重要的三个阴影关键: ambient shadow, spot shadow, 以及elevation.

阴影的实践

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

526795353b9ca2dbedb634bfc0d1a9c6.jpeg

同样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上下载这个背景,

8e312d395d02355fe7dd0c134f9185dd.jpeg

并导入到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一定要有bg, 才会有阴影的可能?

其实我们上面的话不太对, 即这句: "光源对view照下来, 形成了阴影"

而上一节中, 我们修正了这句话, 即应该是: "光源对view的背景照下来, 形成了阴影".

其实这次的修正仍是不对的, 正确的说法是"光源对View的outline provider照下来, 形成了阴影".

Android中View是自带了outline provider的, 默认值就是background. 其它的可选值如下:

7561a6c80e139a2a135a8e6d543bfea5.jpeg

这下其实也就解释了, 为什么要有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.

关注我获取更多知识或者投稿

e57c537072e166463b4dc7f6618c126c.jpeg

c27311398ec4a08d26ce351e7daa13aa.jpeg

猜你喜欢

转载自blog.csdn.net/c6E5UlI1N/article/details/131928857