デザイン ドラフトに影を追加する必要があると記載されている場合、Android では影の実装方法が Web や iOS とは異なるため、常に注意が必要です。
作者:Lowae
链接:https://juejin.cn/post/7270503053358874664
一般的に、影の通常の形式は次のとおりです。
X: X 軸のオフセット
Y: Y 軸のオフセット
ブラー: 影のブラー半径
色: 影の色
影とは何ですか
ただし、Android では比較的単純であり、測定単位は 1 つだけです:標高. Android 5.0 のマテリアル 2 で導入された概念として、画像を使用してそれを鮮明に説明します. 実際、それは本質的に仮想 Z 軸座標です。
さて、高低差があるので、影を形成するための光源が必要になります。マテリアル 2 では、光源は画面の真上にあるだけでなく、メイン ライトに分割された 2 組の光源があります。以下に示すように、光源 (キーライト) と周囲光源 (周囲光) を設定します。
最終結果は、複合光源からのより自然な影になります。
アンビエント光源には画面空間内の実際の位置がありませんが、メイン光源には実際の位置があります。特定のパラメータについては、次を参照してください。
Frameworks/base/core/res/res/values/dimens.xml - Android コード検索
さて、シャドウ自体のメカニズムがわかったので、次のステップはシャドウをカスタム制御する方法です。これがこの記事の目的でもあります。
SDK21からは影のぼかし半径のような効果を実現するElevationが用意されていますが、結局スケールが単一すぎて必要な効果を満たせない場合があるため、色の制御も必要となります。シャドー。
outlineSpotShadowColor
SDK 28以降では、キーライトとアンビエントライトが落とす影の色をそれぞれとで設定できるようになりましたoutlineAmbientShadowColor
が、正直言ってこの2つのプロパティは基本的に役に立たないか役に立たないです。
しかし、ここではアウトラインという概念が導入されています。
4 つの一般的なオプション
立面図 + 輪郭線
アウトラインは、実際にはビューの境界線 (アウトライン) です。OutlineProvider を使用して、ビューのアウトラインをカスタマイズして、立面図の下でのビュー自体の投影に影響を与えることができます。たとえば、丸みを帯びた 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.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)
}
}
効果は次のとおりです: (ただしoutlineAmbientShadowColor
、outlineSpotShadowColor
SDK 28 以降のみをサポートします)
通常、このステップでは、高さの値とアウトライン、および上位バージョンで利用可能な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)
}
}
しかし、前に述べた 2 つの光源を覚えていますか? 光源の 1 つが画面の斜め上に配置されているため、別の問題が発生します。次の図に示すように、同じ View では同じ Elevation が設定され、Y 軸座標が異なるとその影の効果が異なります。
つまり、影の Blur パラメータと Color パラメータはほとんど満たされません。
長所: ネイティブのシャドウ効果
欠点: 影の色の設定には SDK>=28 が必要で、アウトラインを使用して影の輪郭を制御する必要があります。
まず、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 軸のオフセットのみをサポートしています。
利点: 箱から出してすぐに描画可能で、角が丸くなっています。
短所: シミュレートされたシャドウ効果、表示効果が十分に正確ではなく、非効率的
ナインパッチドローアブル
Android では正直、単純な影を実装するのは難しすぎます。この Web サイト経由: Android Shadow Generator (inloop.github.io)
CSS スタイルのシャドウ効果を直接生成でき、Figma のシャドウ効果をほぼ完全に復元できます。その効果は次のとおりです。
実は非常に還元的ですが、角が丸いという致命的な欠陥があります。写真なので、Androidでは角丸の単位は基本的にdpではなくpxです。角が丸い影が必要な場合はそうではありません。期待通りです。
利点: 完全に制御可能なパラメータを備えたシャドウにより、デザイン ドラフトを 1:1 で復元できます。
欠点: 画像であるため、影の丸い角はピクセル密度に合わせて拡大縮小できません (非常に致命的な欠点)
Paint.setShadowLayer/BlurMaskFilter
これら 2 つをまとめた理由は、基本的に次のような実装が似ているためです。
paint.setShadowLayer(radius, offsetX, offsetY, shadowColor)
// 或者使用maskFilter然后通过paint.color以及绘制的区域进行offset来变相控制阴影的Color及offset
paint.maskFilter = BlurMaskFilter(field, BlurMaskFilter.Blur.NORMAL)
相比之下更推荐使用setShadowLayer,最终效果如下,基本上没啥问题:
ただし、これによって描画される影は基本的に BlurMaskFilter と同等であることに注意してください。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 メソッドに焦点を当てますが、これら 2 つのメソッドで実現される影が一貫していると言えるのはなぜでしょうか。これには、C++ 層を深く掘り下げる必要があります。まず、paint.setShadowLayer のネイティブ実装クラスに直接ジャンプします: Frameworks/base/libs/hwui/jni/Paint.cpp
Paint.cpp - Android コード検索
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}));
}
}
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 コード検索
/**
* 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;
}
これにより、正確な値ではありませんが、少なくとも描画された影がクリップされないことを保証できます。もちろんPaddingの予約が不可能な場合でも実現可能ですclipChildren=false
。
要約する
最後に、setShadowLayer のカスタム View 実装も提供しました。
Lowae/Shadows: CSS スタイルのシャドウを実装するための Android 上のシンプルでカスタマイズ可能なライブラリ (github.com)
興味があれば試してみてください。互換性の問題がある場合は、問題を提起してください~
(互換性の問題が多々あるのは重々承知しています。仕方がありません。この種の API はこうです。いえ、正確に言うと Android はこうです)
そのため、Android 上でデザイン案の影を 1:1 で復元することは困難ですが、パラメータの復元を追求せず、見た目の多少の統一性を求めるだけであれば、まだ可能です。 .メソッド (Elevation + Outline)、影の色またはオフセットを設定する場合は、最後のメソッド (setShadowLayer) を試すことができます。
より多くの知識を得る、または記事に貢献するには私をフォローしてください