Fork me on GitHub

Android 边框阴影绘制方案

在说解决方案之前,我们需要先明确两个问题

  1. 到底什么是阴影
  2. 为什么在 Android 端上绘制阴影效果这么麻烦

每当我向 Android 同学提出第一问时,通常他们会回答我:阴影是个10-25%透明度的线性减淡的黑色图层

但是当你尝试在移动端去绘制这样一个黑色图层去代表阴影,其视觉效果肉眼可见的差强人意

于是绝大多数 Android 同学会提议你去找设计出个带阴影的图,或者使用诸如 ShadowLayout 这样复杂的控件
https://github.com/lihangleo2/ShadowLayout

ShadowLayout 使用了 1600 行代码只为实现边框阴影

再看看 github 上其他绘制阴影的方案,不是采用 RenderScript 这种即将被谷歌废弃的图像处理库,就是使用 Android 12 以上才有的还要依赖硬件加速的 RenderEffect,更有甚者者干脆用 Canvas 硬画,不仅效果不好,性能更是很差,稍有复杂的情况,甚至可能导致卡顿发热

我们不经要发问:Android 10多年了,难道就没有一个不需要和设计掰扯,能兼容所有机型,对开发简单友好的阴影解决方案吗?

到底什么是阴影

先来看下阴影的组成部分

我们可以将阴影大致分成两个部分,全影/umbra半影/penumbra

半影区域就是阴影的过渡区, 也就是软阴影,有半影的阴影过渡时,视觉效果会好很多。

pEWsX28.md.png

通俗点说:全影就是大家常说的 10 - 25% 透明度的黑色图层,通常我们需要绘制的边框阴影是半影。

为什么在 Android 端上绘制阴影效果这么麻烦

半影的绘制实际上是一个多次采样模糊的过程

1
2
3
4
5
6
7
8
9
10
11
float PCF(float2 uv, float currentDepth) {
float shadow = 0.0;
float2 texelSize = 1.0 / _ShadowMap_TexelSize;
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
float depth = tex2D(_ShadowMap, uv + float2(x, y) * texelSize).r;
shadow += (currentDepth > depth) ? 1.0 : 0.0;
}
}
return shadow / 9.0;
}

在上述代码中,对 3* 3 的区域进行了模糊采样,也就是说每个像素点的颜色依赖其周围相邻的一圈像素点的颜色的计算结果,实现逐步衰减

切图和代码实现究竟哪个更好?

首先明确一点:切图和代码实现的本质区别是什么?

是预渲染和实时渲染的区别

如果我们当前卡片所在的背景是可枚举且数量较少的,我们应该倾向使用预渲染

举个例子:

  1. 如果一个页面只有日夜间两种阴影我们倾向于切图
  2. 如果需要七彩渐变非中心光源产生的(上下左右阴影不一致的)阴影,我们倾向于代码实现

使用 .9 图来规避切图的弊端

切图虽然方便,却有以下几种弊端

  1. 设计会想打你,尤其当你需要反复调试效果的时候
  2. 切图会增加包体积

解决方案

  1. 自己学会使用 figma 调试阴影切图

pEWy1G6.md.png

  1. 使用 .9 图

pEWsHUI.png

当我们想要绘制一个带阴影的圆角矩形的时候,我们会发现除了矩形的四个角外,整个矩形绝大部份都是重复可拉伸的

如果我切出一个这样的圆角矩形最小图,并设置其非圆角的区域为拉伸区域

那我们就可以用一个只有几 kb 大小的 .9 图实现固定圆角的带边框阴影的矩形

考虑到实际情况,我们可以切出 0 - 12度的圆角图共13张图,圆角越小的图片越小,易得总共大约 (1kb + 5kb ) / 2 * 13 = 39kb

再算上深色模式,未打包压缩前总共只需要 80kb

而 1600 行的 ShadowLayout.java 未压缩前有 55kb,引入 Androidx 的 CardView 就更大了,还需要考虑机型的兼容性和硬件加速开关

而使用 .9 图,开发同学只需要选择对应圆角的 .9 图设置为背景即可

使用

轻松使用

1
2
3
4
5
<View
android:layout_width="400dp"
android:layout_height="200dp"
android:backgroud="@drawable/rounded_corner_12dp_shadow"
/>

展望

我们可以进一步,将圆角和矩形分离,使用原子化的图片素材 + 微量代码,组合各种圆角与阴影效果

总结

以下是 预渲染(.9图)代码实现(如ShadowLayout / CardView) 的优劣势对比表格,结合性能、开发效率、灵活性等维度分析:


对比维度 预渲染(.9图) 代码实现(动态绘制)
性能 ✅ 极低开销(仅纹理采样) ⚠️ 较高开销(实时计算阴影、图层叠加)
内存占用 ✅ 小 ⚠️ 动态(取决于具体代码)
开发效率 ✅ 直接设置背景,无需编码 ⚠️ 需编写/维护复杂逻辑(如阴影算法)
灵活性 ❌ 仅支持预定义的圆角/阴影样式 ✅ 动态调整参数(颜色、圆角、模糊度等)
适配性 ✅ 无兼容性问题(Android/iOS通用) ⚠️ 需处理机型兼容(如硬件加速、API差异)
扩展性 ❌ 修改样式需重新切图 ✅ 可通过代码快速迭代新效果
文件体积 ✅ 极小(如80KB未压缩) ⚠️ 较大(代码+依赖库,如CardView)
动态效果支持 ❌ 无法实现动画或实时变化 ✅ 支持动态阴影(如跟随光源移动)
多分辨率适配 ✅ 自动拉伸(.9图特性) ⚠️ 需额外处理多分辨率下的绘制精度
维护成本 ✅ 低(替换图片即可) ⚠️ 高(需测试不同机型/系统版本)

适用场景总结

  1. 优先使用.9图
    • 样式固定(如标准化设计语言中的卡片阴影)。
    • 性能敏感场景(如列表项、高频刷新界面)。
    • 需要快速开发且无动态效果需求。

  2. 优先代码实现
    • 需要动态效果(如阴影动画、交互反馈)。
    • 样式高度自定义(如设计师要求的特殊渐变阴影)。

补充建议

混合方案:对静态部分使用.9图,动态部分叠加代码绘制(如悬浮按钮的阴影)。
工具化:将.9图生成和代码阴影封装成工具,供团队按需选择

通过明确场景需求,可以高效选择最优方案。

,