论如何在Android中还原设计稿中的阴影

news2024/9/23 23:33:43

每当设计稿上注明需要添加阴影时,Android上总是显得比较棘手,因为Android的阴影实现方式与Web和iOS有所区别。

一般来说阴影通常格式是有:

X: 在X轴的偏移度

Y: 在Y轴偏移度

Blur: 阴影的模糊半径

Color: 阴影的颜色

何为阴影

但是在Android中却比较单一,只有一个度量单位:Elevation,作为在Android5.0的material2引入的概念,用一个图来形象的描绘一下,其实本质上就是虚拟的Z轴坐标。

image.png

那好,高度差有了,还差个光源,这样才能形成阴影,在material2中,光源不是单一的位于屏幕正上方的,而且有两组光源,分为主光源(Key light)和环境光源(Ambient light)如下图所示:
image.png

最终形成的效果是一种复合光源下更自然的阴影。

image.png

其中环境光源,在屏幕空间中没有实际的位置,但是主光源是有实际的位置的,具体的参数见:

frameworks/base/core/res/res/values/dimens.xml - Android Code Search
image.png

好,既然知道了阴影本身的机制,那下一步现在则是如何自定义控制阴影,这也是本文的目的。

从SDK 21开始,提供了Elevation可以实现类似于阴影的模糊半径的效果,但是毕竟尺度过于单一,往往有时候无法满足所需的效果,所以,还需要控制阴影的颜色。

在SDK 28之后,可以通过outlineSpotShadowColoroutlineAmbientShadowColor来分别设置Key light和Ambient light投射的阴影颜色,但是说实话,这两个属性基本用不到或者说比较鸡肋。

不过这里引入了一个概念:Outline。

四种常见方案

Elevation + Outline

Outline其实是View的边框(轮廓),通过OutlineProvider可以自定义一个View的Outline从而影响View本身在elevation下的投影,比如定义以实现一个圆角ImageView为例:

<ImageView
    android: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 ?: return
        outline?.setRoundRect(0, 0, view.width, view.height, 32f)
    }

}

效果基本没啥问题:
image.png

同样的,既然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 ?: return
        outline?.setRoundRect(0, 0, view.width, view.height, 32f)
    }

}

效果如下:(不过outlineAmbientShadowColoroutlineSpotShadowColor仅支持SDK 28及以上)

image.png

通常,到这一步通过调整elevation的数值和outline以及高版本可用的shadowColor大体上可以满足设计师的阴影需求。
而且通常来说shadowColor都是Color.Black以及alpha的区别,所以你也可以这样:

outlineProvider = object : ViewOutlineProvider() {
    override fun getOutline(view: View?, outline: Outline?) {
        view ?: return
        outline?.alpha = 0.5f
        outline?.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

效果如图:
image.png

只能说很一般,毕竟是模拟的阴影模糊效果,而且目前只支持Y轴的offset。

优点:几乎是开箱即用的Drawable且自带圆角

缺点:模拟的阴影效果,展示效果不够精细且效率不高

NinePatchDrawable

说实话想在Android上实现一个简单的阴影太折腾了,什么奇怪的技巧都来了,比如.9图,至于什么是.9图这里便不再过多介绍。
通过这个网站:Android Shadow Generator (inloop.github.io)

image.png
你可以直接生成一个CSS Style的阴影效果,几乎可以完美还原Figma的阴影效果,效果如下:
image.png

其实还是很还原的,但是它有一个致命的缺点,就是圆角,因为是一张图片,所以圆角的单位本质上是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,最终效果如下,基本上没啥问题:
image.png

但是值得注意的是,其绘制的阴影本质上等价于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 sRGB
        color = 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)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1014592.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

手刻 Deep Learning -第壹章 -PyTorch教学-激励函数与感知机入门(上)

一、前言 本文接续前篇教学 Pytorch 与线性回归 &#xff0c;本文着重在 Activation Function &#xff08; 中文称 激励函数 &#xff09;&#xff0c;我们会介绍激励函数 &#xff08;也有人称 激活函数&#xff1f; 激发函数&#xff1f; &#xff09; 为什么会有用&#xf…

Python 06 之面向对象基础

&#x1f600;前言 在日常编程和软件开发中&#xff0c;我们通常会遇到各种各样的问题&#xff0c;其中很多问题都可以通过面向对象的程序设计方法来解决。面向对象编程不仅可以使代码更加组织化和系统化&#xff0c;而且还可以提高代码的重用性和可维护性。 . 在本教程中&…

一个工位的演讲

前几天&#xff0c;知乎官方制作了一个视频&#xff0c;感觉这个视频非常不错&#xff0c;推荐给大家看看。 这个短片很有创意&#xff0c;而且&#xff0c;内容也很丰富。 为什么说这个短片很有创意呢&#xff1f;因为它是从一个工位的角度切入的&#xff0c;所谓铁打的工位&a…

基于matlab实现的电力系统稳定性分析摆幅曲线代码

完整程序&#xff1a; clear; clc; t 0; tf 0; tfl 0.5; tc 0.5; % tc 0.05, 0.125, 0.5 sec for 2.5 cycles, 6.25 cycles & 25 cycles resp ts 0.05; m 2.52 / (180 * 50); i 2; dt 21.64 * pi / 180; ddt 0; time(1) 0; ang(1) 21.64; pm 0.9; pm1 2.44;…

解锁大数据宝藏:使用AI预测未来趋势的新方法

文章目录 大数据的价值传统方法与AI的优势使用AI预测未来趋势的新方法1. 时间序列分析2. 自然语言处理&#xff08;NLP&#xff09;3. 预测市场趋势 应用前景与挑战应用前景挑战和限制 结论 &#x1f389;欢迎来到AIGC人工智能专栏~解锁大数据宝藏&#xff1a;使用AI预测未来趋…

软件设计师考试学习1

前言 计算机组成原理及体系结构 数据的表示 进制的转换 原码反码补码移码 最高位是符号位&#xff0c;负数符号位为1 反码补码正数和原码一样&#xff0c;负数有区别 反码符号位不动&#xff0c;其他位置取反 补码在反码基础上加1 移码是将补码的符号为取反 在原码和反码中…

Sqlserver如何调试存储过程

前提&#xff1a;需要使用Sql Server Managerment Studio 工具进行调试。 步骤&#xff1a; 1.选择存储过程&#xff0c;右键选择“执行存储过程”。操作过后&#xff0c;会生成一个调用存储过程的代码块。 2.以编辑模式打开所以需要调试的存储过程。 3.点击调试按钮进行调…

k8s集群使用ingress转发grafana服务

文章目录 前言一、思路二、grafana准备1. grafana-configmap.yaml2. grafana.yaml 三、ingress准备1. ingress.yaml2. grafana-externalname.yaml3. ingress-nginx-controller 四、 本机host文件准备五、访问测试 前言 在k8s集群中&#xff0c;使用ingress服务转发grafana的页…

苹果是真牛,数据线的钱要赚,换电池钱也要赚,贪婪成性

iPhone15发布会是苹果当前的热点&#xff0c;发布会结束后苹果又传出了一个消息&#xff0c;进一步提高更换电池的价格&#xff0c;显示出苹果意图进一步割韭菜&#xff0c;提升利润&#xff0c;这就有点贪婪成性了吧&#xff1f; 据了解 iPhone 7、8 及 SE 系列由 528 元涨至 …

商淘云八周年 与实体店业务共发展

引言&#xff1a;商淘云主要从事电商平台、B2B订货系统、供应链管理系统的开发和搭建&#xff0c;目前服务的客户有&#xff1a;中商北斗供应链管理集团有限公司、奥克斯物业、苏州广电总台、中国平安、南方新闻网等。 随着时间的流淌&#xff0c;市场不断发展&#xff0c;商淘…

【多线程案例】阻塞式队列

1. 什么是阻塞式队列 阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则. 阻塞队列能是一种线程安全的数据结构, 并且具有以下特性: 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素. 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往…

性能工程落地的4个阶段

性能工程按照不同的内容和目的划分为4个阶段,分别是线下单系统压测分析阶段、线下全链路压测分析阶段、生产只读业务压测及容量评估阶段、生产读写业务全链路压测及容量评估阶段。(也可以理解为一个企业性能测试体系的发展阶段) 线下单系统压测分析阶段 针对单系统的性能…

第71步 时间序列建模实战:ARIMA建模(Python)

基于WIN10的64位系统演示 一、写在前面 这一期&#xff0c;我们使用Python进行SARIMA模型的构建。 同样&#xff0c;这里使用这个数据&#xff1a; 《PLoS One》2015年一篇题目为《Comparison of Two Hybrid Models for Forecasting the Incidence of Hemorrhagic Fever wi…

LeetCode算法动态规划—剑指 Offer 10- II. 青蛙跳台阶问题

目录 剑指 Offer 10- II. 青蛙跳台阶问题 题解&#xff1a; 代码&#xff1a; 运行结果&#xff1a;​编辑 一只青蛙一次可以跳上1级台阶&#xff0c;也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 答案需要取模 1e97&#xff08;1000000007&#xff…

实现按钮悬停动画

知识点与技巧 伪元素 使用伪元素来作为按钮悬停效果动画展示的元素 z-index 的使用技巧 使用z-index属性来控制按钮和伪元素的层次关系 transform、transition 复习 使用transform、transition两个属性来实现动画的展示 按钮边框动画 切换效果 核心代码 .btn.btn-border-…

Python语言开发环境安装

视频版教程 Python3零基础7天入门实战视频教程 首先去Python官网下载Python安装包 https://www.python.org/ 目前最新版本&#xff1a;Python 3.11.4 直接点击下载。&#xff08;建议用迅雷下载&#xff0c;速度快&#xff09; 双击安装包进行Python安装。 勾选下 Add pytho…

3D生成式AI模型、应用与工具大全

当谈到技术炒作时&#xff0c;人工智能正在超越虚拟世界&#xff0c;吸引世界各地企业和消费者的注意力。 但人工智能可以进一步增强虚拟世界&#xff0c;至少在某种意义上&#xff1a;资产创造。 AI 有潜力扩大用于虚拟环境的 3D 资产的创建。 推荐&#xff1a;用 NSDT编辑器…

二叉树(堆)

堆的性质&#xff1a; 堆中某个节点的值总是不大于或不小于其父节点的值&#xff1b; 堆总是一棵完全二叉树。 大堆&#xff1a;任何父亲≥孩子 小堆&#xff1a;任何父亲≤孩子 接下来&#xff0c;我们要做的便是对堆进行增加和删除&#xff1a; 首先是增加操作&#xff0c…

【算法专题突破】滑动窗口 - 找到字符串中所有字母异位词(14)

目录 1. 题目解析 2. 算法原理 3. 代码编写 写在最后&#xff1a; 1. 题目解析 题目链接&#xff1a;438. 找到字符串中所有字母异位词 - 力扣&#xff08;Leetcode&#xff09; 这道题很好理解&#xff0c;就是找出从不同位置开始的所有异位词。 2. 算法原理 那我们该如…

下载安装python的详细步骤-2023

目录 一、介绍 二、下载 三、安装 四、测试 一、介绍 读者手册&#xff08;必读&#xff09;_csdn文章评分怎么看_云边的快乐猫的博客-CSDN博客 Python 是一种高级编程语言&#xff0c;具有简洁、易读、易学的特点。它由Guido van Rossum于1989年首次发布&#xff0c;并于1…