论如何在Android中还原设计稿中的阴影
每当设计稿上注明需要添加阴影时,Android上总是显得比较棘手,因为Android的阴影实现方式与Web和iOS有所区别。
一般来说阴影通常格式是有:
X: 在X轴的偏移度
Y: 在Y轴偏移度
Blur: 阴影的模糊半径
Color: 阴影的颜色
何为阴影
但是在Android中却比较单一,只有一个度量单位:Elevation,作为在Android5.0的material2引入的概念,用一个图来形象的描绘一下,其实本质上就是虚拟的Z轴坐标。
那好,高度差有了,还差个光源,这样才能形成阴影,在material2中,光源不是单一的位于屏幕正上方的,而且有两组光源,分为主光源(Key light)和环境光源(Ambient light)如下图所示:
最终形成的效果是一种复合光源下更自然的阴影。
其中环境光源,在屏幕空间中没有实际的位置,但是主光源是有实际的位置的,具体的参数见:
frameworks/base/core/res/res/values/dimens.xml - Android Code Search
好,既然知道了阴影本身的机制,那下一步现在则是如何自定义控制阴影,这也是本文的目的。
从SDK 21开始,提供了Elevation可以实现类似于阴影的模糊半径的效果,但是毕竟尺度过于单一,往往有时候无法满足所需的效果,所以,还需要控制阴影的颜色。
在SDK 28之后,可以通过outlineSpotShadowColor
和outlineAmbientShadowColor
来分别设置Key light和Ambient light投射的阴影颜色,但是说实话,这两个属性基本用不到或者说比较鸡肋。
不过这里引入了一个概念:Outline。
四种常见方案
Elevation + Outline
Outline其实是View的边框(轮廓),通过OutlineProvider可以自定义一个View的Outline从而影响View本身在elevation下的投影,比如定义以实现一个圆角ImageView为例:
<ImageViewandroid:id="@+id/image"android:layout_width="100dp"android:layout_height="100dp"android:src="@color/material_dynamic_primary90" />
image.clipToOutline = true
image.outlineProvider = object : ViewOutlineProvider() {override fun getOutline(view: View?, outline: Outline?) {view ?: returnoutline?.setRoundRect(0, 0, view.width, view.height, 32f)}}
效果基本没啥问题:
同样的,既然View的轮廓变化了,阴影自然也会跟着随之变化,所以outline也可以改变阴影:
image.elevation = 32f
image.outlineAmbientShadowColor = Color.RED
image.outlineSpotShadowColor = Color.BLUE
image.clipToOutline = true
image.outlineProvider = object : ViewOutlineProvider() {override fun getOutline(view: View?, outline: Outline?) {view ?: returnoutline?.setRoundRect(0, 0, view.width, view.height, 32f)}}
效果如下:(不过outlineAmbientShadowColor
和outlineSpotShadowColor
仅支持SDK 28及以上)
通常,到这一步通过调整elevation的数值和outline以及高版本可用的shadowColor大体上可以满足设计师的阴影需求。
而且通常来说shadowColor都是Color.Black以及alpha的区别,所以你也可以这样:
outlineProvider = object : ViewOutlineProvider() {override fun getOutline(view: View?, outline: Outline?) {view ?: returnoutline?.alpha = 0.5foutline?.setRoundRect(0, 0, view.width, view.height, 32f)}
}
但是,还记着前面提到的两个光源吗?其中有一个光源是位于屏幕斜上方的,这就带来了另外一个问题,同一个View设置相同的Elevation在不同的Y轴坐标它的阴影效果是不一样的,如下图所示:

总之,阴影的Blur和Color参数勉强是可以得到满足的。
优点:原生的阴影效果
缺点:设置阴影的颜色需要SDK>=28,需要配合使用outline来实现对阴影的轮廓控制
下面我们先来引申一下Android中了解过的阴影实现方式。
LayerDrawable
我相信大家肯定见过这种实现方式,通过绘制一层层渐变色来模拟阴影,其实官方也有通过该方式实现的阴影:MaterialShapeDrawable
,示例如下:
val drawable = MaterialShapeDrawable(ShapeAppearanceModel.builder().setAllCornerSizes(16.dp).build()
)
drawable.fillColor = ColorStateList.valueOf(getColor(com.google.android.material.R.color.material_dynamic_primary90))
drawable.setShadowColor(Color.RED)
drawable.shadowVerticalOffset = 8.dp.toInt()
drawable.elevation = 32f
drawable.shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
image.background = drawable
效果如图:
只能说很一般,毕竟是模拟的阴影模糊效果,而且目前只支持Y轴的offset。
优点:几乎是开箱即用的Drawable且自带圆角
缺点:模拟的阴影效果,展示效果不够精细且效率不高
NinePatchDrawable
说实话想在Android上实现一个简单的阴影太折腾了,什么奇怪的技巧都来了,比如.9图,至于什么是.9图这里便不再过多介绍。
通过这个网站:Android Shadow Generator (inloop.github.io)
你可以直接生成一个CSS Style的阴影效果,几乎可以完美还原Figma的阴影效果,效果如下:
其实还是很还原的,但是它有一个致命的缺点,就是圆角,因为是一张图片,所以圆角的单位本质上是px而非Android上的dp,如果你需要一个带圆角弧度的阴影是达不到预期的。
优点:参数完全可控的阴影,可以做到1:1还原设计稿
缺点:因为是图片,所以阴影的圆角无法跟随像素密度缩放(非常致命的缺点)
Paint.setShadowLayer/BlurMaskFilter
这两个我之所以放在一起本质上是因为实现起来都是类似的,
如:
paint.setShadowLayer(radius, offsetX, offsetY, shadowColor)
// 或者使用maskFilter然后通过paint.color以及绘制的区域进行offset来变相控制阴影的Color及offset
paint.maskFilter = BlurMaskFilter(field, BlurMaskFilter.Blur.NORMAL)
相比之下更推荐使用setShadowLayer,最终效果如下,基本上没啥问题:
但是值得注意的是,其绘制的阴影本质上等价于BlurMaskFilter,是占位的,而且是需要留出空间来展示的,所以必要时需要对父布局设置android:clipChildren="false"
或者预留出足够的空间。
优点:
1. 参数完全可控的阴影,可以做到1:1还原设计稿
2. 参数的自定义程度及可控性强
缺点:
1. 阴影占位,需要通过clipChildren=false来或者预留空间规避
2. 需要自定义View或者Drawable,写起来较为麻烦。
总的来说,上面介绍了4种可能常见的阴影实现方式,其中按我的经验来说,较为推荐采用Outline或者setShadowLayer的方式来实现,如果可以的话原生Elevation配合Outline基本可以满足大部分需求场景。
当然还有部分实现方式比如用RenderScriptBlur
等等,我没提是因为是前几种方式较为复杂,性价比不高。
Paint.setShadowLayer 扩展内容
下面则重点讲一下Paint.setShadowLayer/BlurMaskFilter这种方式,为什么说这两种方式实现的阴影都是一致的呢?这个就需要深入到C++层。
首先直接跳到paint.setShadowLayer的native实现类:
frameworks/base/libs/hwui/jni/Paint.cpp
Paint.cpp - Android Code Search
static void setShadowLayer(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle, jfloat radius,jfloat dx, jfloat dy, jlong colorSpaceHandle,jlong colorLong) {SkColor4f color = GraphicsJNI::convertColorLong(colorLong);sk_sp<SkColorSpace> cs = GraphicsJNI::getNativeColorSpace(colorSpaceHandle);Paint* paint = reinterpret_cast<Paint*>(paintHandle);if (radius <= 0) {paint->setLooper(nullptr);}else {SkScalar sigma = android::uirenderer::Blur::convertRadiusToSigma(radius);paint->setLooper(BlurDrawLooper::Make(color, cs.get(), sigma, {dx, dy}));}}
里面将我们传入的阴影radius参数转为Sigma并创建了BlurDrawLooper,我们来看看其实现
#include "BlurDrawLooper.h"
#include <SkMaskFilter.h>namespace android {BlurDrawLooper::BlurDrawLooper(SkColor4f color, float blurSigma, SkPoint offset): mColor(color), mBlurSigma(blurSigma), mOffset(offset) {}BlurDrawLooper::~BlurDrawLooper() = default;SkPoint BlurDrawLooper::apply(Paint* paint) const {paint->setColor(mColor);if (mBlurSigma > 0) {paint->setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, mBlurSigma, true));}return mOffset;
}sk_sp<BlurDrawLooper> BlurDrawLooper::Make(SkColor4f color, SkColorSpace* cs, float blurSigma,SkPoint offset) {if (cs) {SkPaint tmp;tmp.setColor(color, cs); // converts color to sRGBcolor = tmp.getColor4f();}return sk_sp<BlurDrawLooper>(new BlurDrawLooper(color, blurSigma, offset));
}} // namespace android
内容不多,可以看到本质上还是利用了setMaskFilter来实现的。
然后还剩下一个点就是通过SkMaskFilter::MakeBlur
生成的模糊是占位的,如果能知道模糊具体需要多大的空间,就可以方便的进行预留以免实际展示时阴影被裁剪。
MakeBlur
最终返回的是一个SkBlurMaskFilterImpl
对象,我们可以先看一下其父类SkMaskFilterBase
的虚函数:重点关注computeFastBounds
函数
SkMaskFilterBase.h - Android Code Search
/*** The fast bounds function is used to enable the paint to be culled early* in the drawing pipeline. This function accepts the current bounds of the* paint as its src param and the filter adjust those bounds using its* current mask and returns the result using the dest param. Callers are* allowed to provide the same struct for both src and dest so each* implementation must accommodate that behavior.** The default impl calls filterMask with the src mask having no image,* but subclasses may override this if they can compute the rect faster.*/virtual void computeFastBounds(const SkRect& src, SkRect* dest) const;
可以看到该函数的作用便是计算MaskFiter的bounds,看一下子类的SkBlurMaskFilterImpl
的实现
void SkBlurMaskFilterImpl::computeFastBounds(const SkRect& src,SkRect* dst) const {// TODO: if we're doing kInner blur, should we return a different outset?// i.e. pad == 0 ?SkScalar pad = 3.0f * fSigma;dst->setLTRB(src.fLeft - pad, src.fTop - pad,src.fRight + pad, src.fBottom + pad);
}
其中fSigme便是最开始通过convertRadiusToSigma(radius)
获取到的返回值,其计算方式如下:
SkBlurMask.cpp - Android Code Search
// This constant approximates the scaling done in the software path's
// "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)).
// IMHO, it actually should be 1: we blur "less" than we should do
// according to the CSS and canvas specs, simply because Safari does the same.
// Firefox used to do the same too, until 4.0 where they fixed it. So at some
// point we should probably get rid of these scaling constants and rebaseline
// all the blur tests.
static const SkScalar kBLUR_SIGMA_SCALE = 0.57735f;SkScalar SkBlurMask::ConvertRadiusToSigma(SkScalar radius) {return radius > 0 ? kBLUR_SIGMA_SCALE * radius + 0.5f : 0.0f;
}
这样,我们可以得到一个模糊的近似Bound,虽然不是一个准确的值但是至少可以保证绘制的阴影不会被裁剪。
当然,如果无法预留Padding也可以通过clipChildren=false
来实现。
总结
最后我也是针对setShadowLayer提供了一个自定义View的实现方式:
Lowae/Shadows: A simple and customizable library on Android to implement CSS style shadows (github.com)
感兴趣的可以尝试使用,有任何兼容性问题欢迎提issue~
(我十分清楚会有很多兼容性问题,没办法,这种Api就是这样,不,准确来说,Android就是这样)
所以,想在Android上1:1还原设计稿上的阴影是比较困难的,但是如果不去追求参数的还原只是寻求视觉的略显一致,那还是可以做到的,简单点的通过第一种方式(Elevation + Outline),如果设置到阴影颜色或者offset这种便可以尝试最后一种方式(setShadowLayer)。
相关文章:

论如何在Android中还原设计稿中的阴影
每当设计稿上注明需要添加阴影时,Android上总是显得比较棘手,因为Android的阴影实现方式与Web和iOS有所区别。 一般来说阴影通常格式是有: X: 在X轴的偏移度 Y: 在Y轴偏移度 Blur: 阴影的模糊半径 Color: 阴影的颜色 何为阴影 但是在A…...

Hadoop生态圈中的Flume数据日志采集工具
Hadoop生态圈中的Flume数据日志采集工具 一、数据采集的问题二、数据采集一般使用的技术三、扩展:通过爬虫技术采集第三方网站数据四、Flume日志采集工具概述五、Flume采集数据的时候,核心是编写Flume的采集脚本xxx.conf六、Flume案例实操1、采集一个网络…...
FFmpeg获取媒体文件的视频信息
视频包标志位 代码 printf("index:%d\n", in_stream->index);结果 index:0视频帧率 // avg_frame_rate: 视频帧率,单位为fps,表示每秒出现多少帧 printf("fps:%lffps\n", av_q2d(in_stream->avg_frame_rate));结果 fps:29.970070fps…...

io概述及其分类
一、IO概念 • I/O 即输入Input/ 输出Output的缩写,其实就是计算机调度把各个存储中(包括内存和外部存储)的数据写入写出的过程; I : Input O : Output 通过IO可以完成硬盘文件的读和写。 • java中用“流(stream&am…...
前端面试话术集锦第 14 篇:高频考点(React常考基础知识点)
这是记录前端面试的话术集锦第十四篇博文——高频考点(React常考基础知识点),我会不断更新该博文。❗❗❗ 1. 生命周期 在V16版本中引入了Fiber机制。这个机制一定程度上的影响了部分生命周期的调用,并且也引入了新的2个API来解决问题。 在之前的版本中,如果你拥有一个很…...
UI/UX+前端架构:设计和开发高质量的用户界面和用户体验
引言 随着数字化和互联网的普及,越来越多的企业和组织需要高质量的用户界面和用户体验,以及可靠、高效的前端架构。UI/UX设计师和前端架构师可以为这些企业和组织提供所需的技术和创意支持。本文将介绍UI/UX前端架构这个方向,包括设计原则、…...

长尾关键词挖掘软件-免费的百度搜索关键词挖掘
嗨,大家好!今天,我想和大家聊一聊长尾关键词挖掘工具。作为一个在网络世界里摸爬滚打多年的人,我对这个话题有着一些个人的感悟和见解,希望能与大家分享。 首先,让我坦白一点,长尾关键词挖掘工具…...

React Native 环境配置(mac)
React Native 环境配置(mac) 1.Homebrew2.Node.js、WatchMan3.Yarn4.Android环境配置1.安装JDK2.下载AndroidStudio1.国内配置 Http Proxy2.安装SDK1.首先配置sdk的路径2.SDK 下载 3.创建模拟器4.配置 ANDROID_HOME 环境变量 5.IOS环境1.升级ruby&#x…...

CAD for JS:VectorDraw web library 10.1004.1 Crack
VectorDraw web library经过几年的研究,通过互联网展示或工作的可能性并拒绝了各种项目,我们最终得出的结论是,在 javascript 的帮助下,我们将能够在 Microsoft IE 以外的互联网浏览器中通过网络演示矢量图形(支持 ocx…...

代码管理工具git1
ctrl 加滚轮 放大字体 在计算机任意位置单击右键,选择::Git Bash Here git version git清屏命令:ctrl L查看用户名和邮箱地址: $ git config user.name$ git config user.email修改用户名和邮箱地址:$ git…...

层次聚类分析
1、python语言 from scipy.cluster import hierarchy # 导入层次聚类算法 import matplotlib.pylab as plt import numpy as np# 生成示例数据 np.random.seed(0) data np.random.random((20,1))# 使用树状图找到最佳聚类数 Z hierarchy.linkage(data,methodweighted,metric…...

Jmeter性能实战之分布式压测
分布式执行原理 1、JMeter分布式测试时,选择其中一台作为调度机(master),其它机器作为执行机(slave)。 2、执行时,master会把脚本发送到每台slave上,slave 拿到脚本后就开始执行,slave执行时不需要启动GUI࿰…...

学信息系统项目管理师第4版系列08_管理科学基础
1. 科学管理的实质 1.1. 反对凭经验、直觉、主观判断进行管理 1.2. 主张用最好的方法、最少的时间和支出,达到最高的工作效率和最大的效果 2. 资金的时间价值与等值计算 2.1. 资金的时间价值是指不同时间发生的等额资金在价值上的差别 2.2. 把资金存入银行&…...

从2023蓝帽杯0解题heapSpary入门堆喷
关于堆喷 堆喷射(Heap Spraying)是一种计算机安全攻击技术,它旨在在进程的堆中创建多个包含恶意负载的内存块。这种技术允许攻击者避免需要知道负载确切的内存地址,因为通过广泛地“喷射”堆,攻击者可以提高恶意负载被…...

基于SSM的学生宿舍管理系统设计与实现
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:采用JSP技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目&#x…...

jvm 内存模型介绍
一、类加载子系统 1、类加载的过程:装载、链接、初始化,其中,链接又分为验证、准备和解析 装载:加载class文件 验证:确保字节流中包含信息符合当前虚拟机要求 准备:分配内存,设置初始值 解析&a…...

用Jmeter进行压测详解
简介: 1.概述 一款工具,功能往往是很多的,细枝末节的地方也很多,实际的测试工作中,绝大多数场景会用到的也就是一些核心功能,根本不需要我们事无巨细的去掌握工具的所有功能。所以本文将用带价最小的方式讲…...

Mysql001:(库和表)操作SQL语句
目录: 》SQL通用规则说明 SQL分类: 》DDL(数据定义:用于操作数据库、表、字段) 》DML(数据编辑:用于对表中的数据进行增删改) 》DQL(数据查询:用于对表中的数…...
甲骨文全区登录地址
日本东部 东京 https://console.ap-tokyo-1.oraclecloud.com https://console.ap-tokyo-1.oraclecloud.com 日本中部 大阪 https://console.ap-osaka-1.oraclecloud.com https://console.ap-osaka-1.oraclecloud.com 韩国中部 首尔 https://console.ap-seoul-1.oraclecloud.c…...
Java面试题第八天
一、Java面试题第八天 1.如何实现对象克隆? 浅克隆 浅克隆就是我们可以通过实现Cloneable接口,重写clone,这种方式就叫浅克隆,浅克隆 引用类型的属性,是指向同一个内存地址,但是如果引用类型的属性也进行浅克隆就是深…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

C++_核心编程_多态案例二-制作饮品
#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...

超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...

MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...