Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

【Android】透過色を不透明色に変換する方法

こんにちは、Androidエンジニアのふなちです😌

今回は、透過で色を指定されたけどその色を不透明色(特定の背景で表示した時の色)で使いたい場合の解決方法を紹介します。

透過色を不透明色に変える方法

結論を先に言うと、ColorUtilsの中にcompositeColorsという関数を使います。

背景が白色だった場合の色が欲しい場合は以下のようになります。

 val combinationColor = ColorUtils.compositeColors(color, Color.valueOf(Color.WHITE))

もう少し詳しく…

compositeColorsですが、compositeという命名なので足し算のように色を合成する関数かと思ったのですが、引数の命名がforegroundとbackgroundとあるように、backgroundの色にforegroundの色をのっけたときの色を返してくれます。

やりたいことを数式で表してみます。

  • 透過色のRGBをそれぞれ  r、g、b
  • 透過色の透過度を \alpha
  • 不透明な白色のRGBをそれぞれ   r_w、g_w、b_w
  • 作り出したい不透明色をそれぞれ   r'、g'、b'

とするとき、透過色を白背景で表示した時の不透明色に変換する式は以下になります。

 \displaystyle
r' = r \cdot \alpha + r_w \cdot (1 - \alpha)\\
g' = g \cdot \alpha + g_w \cdot (1 - \alpha)\\
b' = b \cdot \alpha + b_w \cdot (1 - \alpha)\\

上記のことを踏まえて内部のコードを見てみます。

// API level 26 以上の時のコード
static Color compositeColors(Color foreground, Color background) {
    if (!Objects.equals(foreground.getModel(), background.getModel())) {
        throw new IllegalArgumentException (
            "Color models must match (" + foreground.getModel() + " vs. "
                + background.getModel() + ")");
    }
    // カラースペースも揃えてくれる:神:
    Color s = Objects.equals(background.getColorSpace(), foreground.getColorSpace())
    ? foreground
    : foreground.convert(background.getColorSpace());

    float[] src = s.getComponents();
    float[] dst = background.getComponents();

    float sa = s.alpha();
    // Destination alpha pre-composited ←アルファの計算はここでしてくれているので、事前に自分で2つの色のアルファを足すと1.0fになるように調整する必要がない。
    float da = background.alpha() * (1.0f - sa);
 
    // Index of the alpha component
    @SuppressLint("Range") 
    int ai = background.getComponentCount() - 1;

    // Final alpha: src_alpha + dst_alpha * (1 - src_alpha) 
    dst[ai] = sa + da;

    // Divide by final alpha to return non pre-multiplied color  ←今回の私のパターンだと恩恵はないですが、sa + da < 1.0f の場合に sa + da = 1.0f になるように色の配合割合を計算している。
    if (dst[ai] > 0) {
        sa /= dst[ai];
        da /= dst[ai];
    }

    // Composite non-alpha components
    for (int i = 0; i < ai; i++) {
        dst[i] = src[i] * sa + dst[i] * da;
    }

    return Color.valueOf(dst, background.getColorSpace());
}

コードを見るとわかるように、関数内でアルファの計算をしてくれています。その為、第二引数にはColor.WHITE * (1.0 - color.alpha) ではなく透過度(alpha)が1.0の色を入れてあげます。 もし単純に足し算のように合成したい場合は、blendARGBなどを使うといいと思います。

参考:https://developer.android.com/reference/androidx/core/graphics/ColorUtils

そもそもなぜこのような状況が起きたのか

弊社のプロダクトでは、アプリバーやステータスバーの色をユーザーが自由に変更することができます。

弊プロダクト`ヤプリ`のCMSのスクショ

これは、アプリをビルドし直さなくても更新することができる設定のため動的にアプリバーやステータスバーに設定を反映させます。

アプリバーは透過色での設定でも大丈夫なのですが、ここで問題になるのがステータスバーに色を設定する場合です。

ステータスバーに動的に色を設定する場合は以下のようなコードを書くと思います。

window.statusBarColor = Color.BLACK // 好きな色を設定する。

ここで、好きな色に透過色を設定するとデフォルトのテーマに合わせた背景色が透けて見えてしまい以下のような見た目になってしまいます。

ライトテーマの場合(※Androidのデフォルトテーマ)
ダークテーマの場合(※Androidのデフォルトテーマ)

ライトテーマの方はよく見るとアプリバーとステータスバーの色が違いますし、ダークテーマの方は明らかに求めている色ではないことが分かります。

普通のアプリだとデフォルトのテーマでステータスバーの色を指定できるのでこのようなことは起きないと思うのですが、動的に見た目が変わるかつ、透過色が設定できてしまう場合に、この問題が起こります。 そこで今回のブログで紹介した「透過色を不透明色に変換する方法」が必要になりました🪄🌟

まとめ

今回は、透過色を不透明色に変換する方法を紹介させていただきました。

弊社ではエンジニアを募集しています。興味が湧いた方はぜひカジュアル面談でお話しましょう!

open.talentio.com

最後まで読んでいただき、ありがとうございました♡