On how to restore the shadow in the design draft in Android

Whenever the design draft states that shadows need to be added, it is always more tricky on Android because the way shadows are implemented on Android is different from those on the Web and iOS.

作者:Lowae
链接:https://juejin.cn/post/7270503053358874664

Generally speaking, the usual format of shadows is:

X: Offset in X axis

Y: offset in Y axis

Blur: The blur radius of the shadow

Color: The color of the shadow

what is shadow

However, it is relatively simple in Android, with only one unit of measurement: Elevation . As a concept introduced in material2 of Android 5.0, use a picture to vividly describe it. In fact, it is essentially the virtual Z-axis coordinate.

6575727c10330cd76b88328e232055fb.jpeg

Well, now that the height difference is there, we still need a light source to form a shadow. In material2, the light source is not just located directly above the screen, but there are two sets of light sources, divided into the main light source (Key light) and the ambient light source ( Ambient light) as shown below:a74f3cd1d0891b4f1b0ba5862b3fcc3c.jpeg

The end result is a more natural shadow from a composite light source.

0a3e07be59943aa54bc5953f33cbf75c.jpeg

The ambient light source has no actual position in screen space, but the main light source has an actual position. For specific parameters, see:

frameworks/base/core/res/res/values/dimens.xml - Android Code Search70eaedc7622271b18e21b7bdc0f87bd9.jpeg

Okay, now that we know the mechanism of the shadow itself, the next step is how to custom control the shadow, which is also the purpose of this article.

Starting from SDK 21, Elevation is provided to achieve an effect similar to the blur radius of a shadow. However, after all, the scale is too single and sometimes cannot meet the required effect. Therefore, it is also necessary to control the color of the shadow.

After SDK 28, you can set the shadow colors cast by Key light and Ambient light respectively through outlineSpotShadowColorand outlineAmbientShadowColor, but to be honest, these two properties are basically useless or useless.

But a concept is introduced here: Outline.

Four common options

Elevation + Outline

Outline is actually the border (outline) of the View. Through the OutlineProvider, you can customize the Outline of a View to affect the projection of the View itself under elevation. For example, take the definition of implementing a rounded ImageView as an example:

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


}

The effect is basically no problem:1aab8c9e326e3d32bd7c4dbb8d7ba859.jpeg

Similarly, since the outline of the View changes, the shadow will naturally change accordingly, so the outline can also change the shadow:

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


}

The effect is as follows: (However outlineAmbientShadowColor, outlineSpotShadowColorit only supports SDK 28 and above)

5ecd56d4d09115e114229b767fe4ebf1.jpeg

Usually, at this step, by adjusting the elevation value and outline, as well as the shadowColor available in higher versions, the designer's shadow needs can generally be met. And generally speaking, shadowColor is the difference between Color.Black and alpha, so you can also do this:

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

But, remember the two light sources mentioned earlier? One of the light sources is located diagonally above the screen, which brings about another problem. The same View sets the same Elevation and its shadow effect is different at different Y-axis coordinates, as shown in the following figure:

58ff0f6cefc25650e258b9f1f7d1a67c.jpeg

In short, the Blur and Color parameters of the shadow can barely be satisfied.

Pros: Native shadow effects

Disadvantages: Setting the color of the shadow requires SDK>=28, and you need to use outline to control the outline of the shadow.

Let's first expand on the shadow implementation methods we have learned about in Android.

LayerDrawable

I believe you must have seen this implementation method, which simulates shadows by drawing layers of gradient colors. In fact, there are also official shadows implemented in this way: , the MaterialShapeDrawableexample is as follows:

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

The effect is as shown in the figure:338d714616e8b2e1c80637ad05b7ec45.jpeg

It can only be said to be very general. After all, it is a simulated shadow blur effect, and it currently only supports the offset of the Y axis.

Advantages: Drawable almost out of the box and comes with rounded corners

Disadvantages: simulated shadow effect, display effect is not precise enough and inefficient

NinePatchDrawable

To be honest, it’s too hard to implement a simple shadow on Android. There are all kinds of strange techniques, such as .9 pictures. As for what .9 pictures are, I won’t go into too much detail here. Via this website: Android Shadow Generator (inloop.github.io)

cbd33d0af04cfbcd1bb7706fba1dd6ec.jpegYou can directly generate a CSS Style shadow effect, which can almost perfectly restore the Figma shadow effect. The effect is as follows:5b50197b54563c390cc034a7b2d690e9.jpeg

In fact, it is very reductive, but it has a fatal flaw, which is rounded corners. Because it is a picture, the unit of rounded corners is essentially px instead of dp on Android. If you need a shadow with rounded corners It's not up to expectations.

Advantages: Shadows with fully controllable parameters can restore the design draft 1:1

Disadvantage: Because it is a picture, the rounded corners of the shadow cannot scale with the pixel density (a very fatal shortcoming)

Paint.setShadowLayer/BlurMaskFilter

The reason why I put these two together is essentially because they are similar in implementation, such as:

paint.setShadowLayer(radius, offsetX, offsetY, shadowColor)
// 或者使用maskFilter然后通过paint.color以及绘制的区域进行offset来变相控制阴影的Color及offset
paint.maskFilter = BlurMaskFilter(field, BlurMaskFilter.Blur.NORMAL)
相比之下更推荐使用setShadowLayer,最终效果如下,基本上没啥问题:

However, it is worth noting that the shadow drawn by it is essentially equivalent to the BlurMaskFilter, which is a placeholder and needs to leave space for display, so if necessary, you need to set android:clipChildren="false"or reserve enough space for the parent layout.

advantage:

1. Shadows with fully controllable parameters can restore the design draft 1:1

2. High degree of customization and controllability of parameters

shortcoming:

1. Shadow occupancy needs to be avoided by clipChildren=false or by reserving space.

2. You need to customize View or Drawable, which is more troublesome to write.

In general, the above introduces 4 possible common shadow implementation methods. According to my experience, it is more recommended to use Outline or setShadowLayer. If possible, native Elevation combined with Outline can basically meet most demand scenarios. .

Of course, there are some implementation methods RenderScriptBlur, such as using etc. I didn’t mention them because the first few methods are more complicated and not cost-effective.

Paint.setShadowLayer extended content

Next, we will focus on the Paint.setShadowLayer/BlurMaskFilter method. Why do we say that the shadows achieved by these two methods are consistent? This requires going deep into the C++ layer. First jump directly to the native implementation class of paint.setShadowLayer: 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}));
        }
    }

It converts the shadow radius parameter we passed in to Sigma and created a BlurDrawLooper. Let's take a look at its implementation.

#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

There is not much content, you can see that it is essentially implemented using setMaskFilter.

Then the remaining point is that SkMaskFilter::MakeBlurthe generated blur is a placeholder. If you can know how much space the blur requires, you can easily reserve it to avoid the shadow being cropped during actual display. MakeBlurWhat is finally returned is an SkBlurMaskFilterImplobject. We can first look at SkMaskFilterBasethe virtual functions of its parent class: focus on computeFastBoundsthe function

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

Among them, fSigme is the return value obtained initially convertRadiusToSigma(radius), and its calculation method is as follows: 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;
}

In this way, we can get a vague approximate Bound. Although it is not an accurate value, it can at least ensure that the drawn shadow will not be clipped. Of course, if it is impossible to reserve Padding, it can also be clipChildren=falseachieved.

Summarize

Finally, I also provided a custom View implementation for setShadowLayer:

Lowae/Shadows: A simple and customizable library on Android to implement CSS style shadows (github.com)

If you are interested, you can try it. If you have any compatibility issues, please raise an issue~

(I am very aware that there will be many compatibility issues. There is no way. This kind of API is like this. No, to be precise, Android is like this)

Therefore, it is difficult to restore the shadows on the design draft 1:1 on Android. However, if you do not pursue the restoration of parameters and only seek a slightly consistent visual appearance, it can still be done. Simply pass the first method. Method (Elevation + Outline), if you set the shadow color or offset, you can try the last method (setShadowLayer).

Follow me to get more knowledge or contribute articles

cf0ced6e37bd58f476f6fcc04891803e.jpeg

182e1d20e085aa310acb36ec521233bf.jpeg

Guess you like

Origin blog.csdn.net/c6E5UlI1N/article/details/132632526