chromium/ui/android/java/src/org/chromium/ui/util/ColorUtils.java

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.ui.util;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;

import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;

import org.chromium.base.MathUtils;

/** Helper functions for working with colors. */
public class ColorUtils {
    // Value used by ui::OptionalSkColorToJavaColor() to represent invalid color.
    public static final long INVALID_COLOR = ((long) Integer.MAX_VALUE) + 1;

    private static final float CONTRAST_LIGHT_ITEM_THRESHOLD = 3f;
    private static final float LIGHTNESS_OPAQUE_BOX_THRESHOLD = 0.82f;
    private static final float MAX_LUMINANCE_FOR_VALID_THEME_COLOR = 0.94f;
    private static final float THEMED_FOREGROUND_BLACK_FRACTION = 0.64f;
    private static final float LIGHT_DARK_LUMINANCE_THRESHOLD = 0.5f;

    /** Percentage to darken a color by when setting the status bar color. */
    private static final float DARKEN_COLOR_FRACTION = 0.6f;

    /**
     * @param context <b>Activity</b> context.
     * @return Whether the activity is currently in night mode.
     */
    public static boolean inNightMode(Context context) {
        int uiMode = context.getResources().getConfiguration().uiMode;
        return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
    }

    /** Computes the lightness value in HSL standard for the given color. */
    public static float getLightnessForColor(@ColorInt int color) {
        int red = Color.red(color);
        int green = Color.green(color);
        int blue = Color.blue(color);
        int largest = Math.max(red, Math.max(green, blue));
        int smallest = Math.min(red, Math.min(green, blue));
        int average = (largest + smallest) / 2;
        return average / 255.0f;
    }

    /**
     * Calculates the contrast between the given color and white, using the algorithm provided by
     * the WCAG v2 in http://www.w3.org/TR/WCAG20/#contrast-ratiodef.
     */
    private static float getContrastForColor(@ColorInt int color) {
        float bgR = Color.red(color) / 255f;
        float bgG = Color.green(color) / 255f;
        float bgB = Color.blue(color) / 255f;
        bgR = (bgR < 0.03928f) ? bgR / 12.92f : (float) Math.pow((bgR + 0.055f) / 1.055f, 2.4f);
        bgG = (bgG < 0.03928f) ? bgG / 12.92f : (float) Math.pow((bgG + 0.055f) / 1.055f, 2.4f);
        bgB = (bgB < 0.03928f) ? bgB / 12.92f : (float) Math.pow((bgB + 0.055f) / 1.055f, 2.4f);
        float bgL = 0.2126f * bgR + 0.7152f * bgG + 0.0722f * bgB;
        return Math.abs((1.05f) / (bgL + 0.05f));
    }

    /**
     * @see ColorUtils#overlayColor(int, int, float). Use this when not in an animation.
     */
    public static @ColorInt int overlayColor(@ColorInt int baseColor, @ColorInt int overlayColor) {
        return overlayColor(baseColor, overlayColor, /* fraction= */ 1f);
    }

    /**
     * Overlays a likely transparent color with the amount that it is transparent. This effectively
     * flattens the two colors together into a new opaque color.
     *
     * @param baseColor The base, opaque, color that is beneath the overlay.
     * @param overlayColor The partially transparent color, whose alpha will be used to decide how
     *     much of an effect it will have when blending.
     * @param fraction Extra 0 to 1 multiplier for alpha overlay, useful for animations that want to
     *     animate into a full overlay.
     * @return A fully opaque color that's the result of blending the two.
     */
    public static @ColorInt int overlayColor(
            @ColorInt int baseColor,
            @ColorInt int overlayColor,
            @FloatRange(from = 0f, to = 1f) float fraction) {
        // Similar to #getColorWithOverlay, this math isn't great for transparent base colors.
        // Consider using #blendColorsMultiply instead.
        // TODO(crbug.com/40282487): Enable asserts once status bar stops passing a base
        // color that's partially transparent.
        // assert Color.alpha(baseColor) == 255;

        @FloatRange(from = 0f, to = 1f)
        float alphaAdjustedFraction = Color.alpha(overlayColor) / 255f * fraction;
        @ColorInt int opaqueOverlayColor = getOpaqueColor(overlayColor);
        return getColorWithOverlay(baseColor, opaqueOverlayColor, alphaAdjustedFraction);
    }

    /**
     * Get a color when overlaid with a different color. Input and output colors should be fully
     * opaque, as this approach does not work well with transparency.
     *
     * @param baseColor The base Android color.
     * @param overlayColor The overlay Android color.
     * @param overlayAlpha The alpha |overlayColor| should have on the base color.
     */
    public static @ColorInt int getColorWithOverlay(
            @ColorInt int baseColor,
            @ColorInt int overlayColor,
            @FloatRange(from = 0f, to = 1f) float overlayAlpha) {
        // Transparency is ignored in the logic below, so assert if anyone is passing a color that's
        // not fully opaque. This does incur a minor burden on clients that knowingly want to call
        // this on a partially transparent color, as they have to change the alpha value first.
        // TODO(crbug.com/40282487): Enable asserts once status bar stops passing a base
        // color that's partially transparent.
        // assert Color.alpha(baseColor) == 255;
        assert Color.alpha(overlayColor) == 255;

        int fromRed = Color.red(baseColor);
        int toRed = Color.red(overlayColor);
        int resultRed = Math.round(MathUtils.interpolate(fromRed, toRed, overlayAlpha));

        int fromGreen = Color.green(baseColor);
        int toGreen = Color.green(overlayColor);
        int resultGreen = Math.round(MathUtils.interpolate(fromGreen, toGreen, overlayAlpha));

        int fromBlue = Color.blue(baseColor);
        int toBlue = Color.blue(overlayColor);
        int resultBlue = Math.round(MathUtils.interpolate(fromBlue, toBlue, overlayAlpha));

        return Color.rgb(resultRed, resultGreen, resultBlue);
    }

    /**
     * Darkens the given color to use on the status bar.
     *
     * @param color Color which should be darkened.
     * @return Color that should be used for Android status bar.
     */
    public static @ColorInt int getDarkenedColorForStatusBar(@ColorInt int color) {
        return getDarkenedColor(color, DARKEN_COLOR_FRACTION);
    }

    /**
     * Darken a color to a fraction of its current brightness.
     *
     * @param color The input color.
     * @param darkenFraction The fraction of the current brightness the color should be.
     * @return The new darkened color.
     */
    public static @ColorInt int getDarkenedColor(@ColorInt int color, float darkenFraction) {
        float[] hsv = new float[3];
        Color.colorToHSV(color, hsv);
        hsv[2] *= darkenFraction;
        return Color.HSVToColor(hsv);
    }

    /**
     * Check whether lighter or darker foreground elements (i.e. text, drawables etc.) should be
     * used depending on the given background color.
     *
     * @param backgroundColor The background color value which is being queried.
     * @return Whether light colored elements should be used.
     */
    public static boolean shouldUseLightForegroundOnBackground(@ColorInt int backgroundColor) {
        return getContrastForColor(backgroundColor) >= CONTRAST_LIGHT_ITEM_THRESHOLD;
    }

    /**
     * Check which version of the textbox background should be used depending on the given color.
     *
     * @param color The color value we are querying for.
     * @return Whether the transparent version of the background should be used.
     */
    public static boolean shouldUseOpaqueTextboxBackground(@ColorInt int color) {
        return getLightnessForColor(color) > LIGHTNESS_OPAQUE_BOX_THRESHOLD;
    }

    /**
     * Returns an opaque version of the given color.
     *
     * @param color Color for which an opaque version should be returned.
     * @return Opaque version of the given color.
     */
    public static @ColorInt int getOpaqueColor(@ColorInt int color) {
        return color | 0xFF000000;
    }

    /**
     * Determine if a theme color is too bright. A theme color is too bright if its luminance is >
     * 0.94.
     *
     * @param color The color to test.
     * @return True if the theme color is too bright.
     */
    public static boolean isThemeColorTooBright(@ColorInt int color) {
        return ColorUtils.getLightnessForColor(color) > MAX_LUMINANCE_FOR_VALID_THEME_COLOR;
    }

    /**
     * Compute a color to use for assets that sit on top of a themed background.
     *
     * @param themeColor The base theme color.
     * @return A color to use for elements in the foreground (on top of the base theme color).
     */
    public static @ColorInt int getThemedAssetColor(@ColorInt int themeColor, boolean isIncognito) {
        if (ColorUtils.shouldUseLightForegroundOnBackground(themeColor) || isIncognito) {
            // Dark theme.
            return Color.WHITE;
        } else {
            // Light theme.
            return ColorUtils.getColorWithOverlay(
                    themeColor, Color.BLACK, THEMED_FOREGROUND_BLACK_FRACTION);
        }
    }

    /**
     * Interpolates between two colors, using pre-multiplied alpha values. Tries to not allocate any
     * new objects or lose any precision unnecessarily.
     *
     * @param from The color to start at, when fraction is at zero.
     * @param to The color to end at, when the fraction is at one.
     * @param fraction The percent through interpolation that's currently being calculated.
     * @return The interpolated color value.
     */
    public static @ColorInt int blendColorsMultiply(
            @ColorInt int from, @ColorInt int to, @FloatRange(from = 0f, to = 1f) float fraction) {
        int fromAlpha = Color.alpha(from);
        int toAlpha = Color.alpha(to);
        // Alpha can be linearly interpolated. Keep the result as float to increase precision of
        // the intermediate math. Lastly, this alpha value can be zero, and we're going to divide by
        // it below. Surprisingly, no special casing is needed. If resultAlpha is zero, this is
        // because one or both of the from/to alphas are also zero, and the color channel
        // interpolation is always going to get zero back. Turns out in Java, 0.0f / 0.0f is NaN,
        // and Math.round special cases this to return 0, which is what we'd want to do anyway.
        float resultAlpha = MathUtils.interpolate(fromAlpha, toAlpha, fraction);

        // Each rgb channel value is multiplied by source alpha before interpolation. Then it'll be
        // divided by the result alpha at the end. This is the pre-multiplied alpha approach as
        // detailed in https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending.
        int fromRed = Color.red(from) * fromAlpha;
        int toRed = Color.red(to) * toAlpha;
        int resultRed = Math.round(MathUtils.interpolate(fromRed, toRed, fraction) / resultAlpha);

        int fromGreen = Color.green(from) * fromAlpha;
        int toGreen = Color.green(to) * toAlpha;
        int resultGreen =
                Math.round(MathUtils.interpolate(fromGreen, toGreen, fraction) / resultAlpha);

        int fromBlue = Color.blue(from) * fromAlpha;
        int toBlue = Color.blue(to) * toAlpha;
        int resultBlue =
                Math.round(MathUtils.interpolate(fromBlue, toBlue, fraction) / resultAlpha);

        return Color.argb(Math.round(resultAlpha), resultRed, resultGreen, resultBlue);
    }

    /**
     * Pass through to {@link androidx.core.graphics.ColorUtils#setAlphaComponent(int, int)} so
     * callers that need methods out of this class don't need to bother importing two versions of
     * classes named "ColorUtils".
     */
    public static @ColorInt int setAlphaComponent(
            @ColorInt int color, @IntRange(from = 0L, to = 255L) int alpha) {
        return androidx.core.graphics.ColorUtils.setAlphaComponent(color, alpha);
    }

    /**
     * Convert the float alpha value into an integer ranging from 0 to 255 to be used in {@link
     * #setAlphaComponent(int, int)}
     */
    public static @ColorInt int setAlphaComponentWithFloat(
            @ColorInt int color, @FloatRange(from = 0f, to = 1f) float alpha) {
        return setAlphaComponent(color, (int) (alpha * 255));
    }

    /**
     * Calculates the luminosity for the given color. This should match Android's region sampling
     * calculation (go/android-luma-calculation).
     *
     * @return The luminance percentage for the color, with {@link Color.BLACK} having a luminance
     *     of 0 and {@link Color.WHITE} have a luminance of 1.
     */
    public static float calculateLuminance(@ColorInt int color) {
        int b = color & 0xFF;
        int g = (color >> 8) & 0xFF;
        int r = (color >> 16) & 0xFF;
        int luminance = (r * 7 + b * 2 + g * 23) / 32;
        return luminance / 255.0f;
    }

    /**
     * Determines whether the luminance is overall high (light) or low (dark). This follows
     * Android's NAVIGATION_LUMINANCE_THRESHOLD.
     */
    public static boolean isHighLuminance(float luminance) {
        return luminance >= LIGHT_DARK_LUMINANCE_THRESHOLD;
    }
}