每当规划稿上注明需求添加暗影时,Android上总是显得比较棘手,由于Android的暗影完成方法与Web和iOS有所区别。

一般来说暗影一般格局是有:

X: 在X轴的偏移度

Y: 在Y轴偏移度

Blur: 暗影的含糊半径

Color: 暗影的色彩

何为暗影

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

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

那好,高度差有了,还差个光源,这样才干形成暗影,在material2中,光源不是单一的坐落屏幕正上方的,并且有两组光源,分为主光源(Key light)和环境光源(Ambient light)如下图所示:

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

终究形成的作用是一种复合光源下更自然的暗影。

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

其间环境光源,在屏幕空间中没有实践的方位,可是主光源是有实践的方位的,详细的参数见:

frameworks/base/core/res/res/values/dimens.xml – Android Code Search

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

好,已然知道了暗影本身的机制,那下一步现在则是怎么自定义操控暗影,这也是本文的意图。

从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)
    }
}

作用根本没啥问题:

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

相同的,已然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及以上)

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

一般,到这一步经过调整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轴坐标它的暗影作用是不一样的,如下图所示:

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

总之,暗影的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

作用如图:

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

只能说很一般,究竟是模仿的暗影含糊作用,并且目前只支撑Y轴的offset。

长处:简直是开箱即用的Drawable且自带圆角

缺陷:模仿的暗影作用,展现作用不够精密且效率不高

NinePatchDrawable

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

论如何在Android中还原设计稿中的阴影
你能够直接生成一个CSS Style的暗影作用,简直能够完美复原Figma的暗影作用,作用如下:
论如何在Android中还原设计稿中的阴影

其实仍是很复原的,可是它有一个丧命的缺陷,便是圆角,由于是一张图片,所以圆角的单位本质上是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,终究作用如下,根本上没啥问题:

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

可是值得注意的是,其绘制的暗影本质上等价于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)。