Using color and color.darker in Android?

JavaAndroidColors

Java Problem Overview


Okay, so I have an integer variable in my application. It's the value of a color, being set by a color picker in my preferences. Now, I need to use both that color and a darker version of any color it might be.

Now I know in standard Java there is a Color.darker() method, but there doesn't seem to be an equivalent in Android. Does anyone know of an equivalent or any workarounds?

Java Solutions


Solution 1 - Java

The easiest, I think, would be to convert to HSV, do the darkening there, and convert back:

float[] hsv = new float[3];
int color = getColor();
Color.colorToHSV(color, hsv);
hsv[2] *= 0.8f; // value component
color = Color.HSVToColor(hsv);

To lighten, a simple approach may be to multiply the value component by something > 1.0. However, you'll have to clamp the result to the range [0.0, 1.0]. Also, simply multiplying isn't going to lighten black.

Therefore a better solution is: Reduce the difference from 1.0 of the value component to lighten:

hsv[2] = 1.0f - 0.8f * (1.0f - hsv[2]);

This is entirely parallel to the approach for darkening, just using 1 as the origin instead of 0. It works to lighten any color (even black) and doesn't need any clamping. It could be simplified to:

hsv[2] = 0.2f + 0.8f * hsv[2];

However, because of possible rounding effects of floating point arithmetic, I'd be concerned that the result might exceed 1.0f (by perhaps one bit). Better to stick to the slightly more complicated formula.

Solution 2 - Java

Here is what I created:

/**
 * Returns darker version of specified <code>color</code>.
 */
public static int darker (int color, float factor) {
    int a = Color.alpha( color );
    int r = Color.red( color );
    int g = Color.green( color );
    int b = Color.blue( color );

    return Color.argb( a,
            Math.max( (int)(r * factor), 0 ),
            Math.max( (int)(g * factor), 0 ),
            Math.max( (int)(b * factor), 0 ) );
}

Solution 3 - Java

Ted's answer to lighten a color wasn't working for me so here is a solution that might help someone else:

/**
 * Lightens a color by a given factor.
 * 
 * @param color
 *            The color to lighten
 * @param factor
 *            The factor to lighten the color. 0 will make the color unchanged. 1 will make the
 *            color white.
 * @return lighter version of the specified color.
 */
public static int lighter(int color, float factor) {
    int red = (int) ((Color.red(color) * (1 - factor) / 255 + factor) * 255);
    int green = (int) ((Color.green(color) * (1 - factor) / 255 + factor) * 255);
    int blue = (int) ((Color.blue(color) * (1 - factor) / 255 + factor) * 255);
    return Color.argb(Color.alpha(color), red, green, blue);
}

Solution 4 - Java

The Java Color routine for darken and brighten doesn't require anything special. In fact, it's simply the unrolled understanding of what brightness is being applied to relevant colors. Which is to say you can simply take the red, green, blue values. Multiply them by any factor, ensure they fall correctly into gamut.

The following is the code found in the Color class.

private static final double FACTOR = 0.7;

//...

public Color darker() {
    return new Color(Math.max((int)(getRed()  *FACTOR), 0),
                     Math.max((int)(getGreen()*FACTOR), 0),
                     Math.max((int)(getBlue() *FACTOR), 0),
                     getAlpha());
}

Obviously, from this we can see how to do this process in android. Take the RGB values, multiply them by a factor, and crimp them into gamut. (Recoded from scratch for licensing reasons).

public int crimp(int c) {
        return Math.min(Math.max(c, 0), 255);
    }

public int darken(int color) {
        double factor = 0.7;
        return (color & 0xFF000000) | 
                (crimp((int) (((color >> 16) & 0xFF) * factor)) << 16) |
                (crimp((int) (((color >> 8) & 0xFF) * factor)) << 8) |
                (crimp((int) (((color) & 0xFF) * factor)));
    }

Do note this is the same thing as just increasing the brightness in HSB, the B is simply the brightest factor, the hue is the ratio between the various colors, and S is how far apart they are. So if we just grab all the colors and multiply them by a factor, we end up with the same colors in the same mix with a bit more white/black in them.

A lot of modern colorspaces also do this with calculating the Y value through the various color components that best approximate brightness. So you could if you wanted convert to a better form of Y or L through any of the modern colorspaces and unconvert them, other colorspaces have a better form of gamma with regard to how much each color contributes to the actual brightness, lightness, value, whiteness, blackness, or whatever that colorspace calls it. This would do a better job, but for most purposes this is solid.

So at the extreme-most you can do this by converting to Lab, decreasing the L component and converting it back.

Here's code to do that:

static int darken(int color) {
    double factor = 0.7;
    double[] returnarray = new double[3];
    convertRGBsRGB(returnarray, ((color >> 16) & 0xFF), ((color >> 8) & 0xFF), (color & 0xFF));
    convertRGBXYZ(returnarray,returnarray[0], returnarray[1], returnarray[2]);
    convertXYZLab(returnarray,returnarray[0], returnarray[1], returnarray[2]);
    returnarray[0] *= factor;
    convertLabXYZ(returnarray,returnarray[0], returnarray[1], returnarray[2]);
    convertXYZRGB(returnarray,returnarray[0], returnarray[1], returnarray[2]);
    return (color & 0xFF000000) | convertsRGBRGB(returnarray);
}
static void convertRGBsRGB(double[] returnarray, int R, int G, int B) {
    double var_R = (((double) R) / 255.0d);                     //RGB from 0 to 255
    double var_G = (((double) G) / 255.0d);
    double var_B = (((double) B) / 255.0d);
    returnarray[0] = var_R;
    returnarray[1] = var_G;
    returnarray[2] = var_B;
}
static int convertsRGBRGB(double[] sRGB) {
    int red = (int) (sRGB[0] * 255);
    int green = (int) (sRGB[1] * 255);
    int blue = (int) (sRGB[2] * 255);
    red = crimp(red);
    green = crimp(green);
    blue = crimp(blue);
    return (red << 16) | (green << 8) | blue;
}
public static int crimp(int v) {
    if (v > 0xff) {
        v = 0xff;
    }
    if (v < 0) {
        v = 0;
    }
    return v;
}

public static final double ref_X = 95.047; //ref_X =  95.047   Observer= 2°, Illuminant= D65
public static final double ref_Y = 100.000; //ref_Y = 100.000
public static final double ref_Z = 108.883;//ref_Z = 108.883
static void convertRGBXYZ(double[] returnarray, double var_R, double var_G, double var_B) {

    if (var_R > 0.04045) {
        var_R = Math.pow(((var_R + 0.055) / 1.055), 2.4);

    } else {
        var_R = var_R / 12.92;

    }
    if (var_G > 0.04045) {
        var_G = Math.pow(((var_G + 0.055) / 1.055), 2.4);

    } else {
        var_G = var_G / 12.92;

    }
    if (var_B > 0.04045) {
        var_B = Math.pow(((var_B + 0.055) / 1.055), 2.4);

    } else {
        var_B = var_B / 12.92;
    }
    var_R = var_R * 100;
    var_G = var_G * 100;
    var_B = var_B * 100; //Observer. = 2°, Illuminant = D65
    double X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805;
    double Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722;
    double Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505;

    returnarray[0] = X;
    returnarray[1] = Y;
    returnarray[2] = Z;
}

static void convertXYZLab(double[] returnarray, double X, double Y, double Z) {
    double var_X = X / ref_X;
    double var_Y = Y / ref_Y;
    double var_Z = Z / ref_Z;

    if (var_X > 0.008856) {
        var_X = Math.cbrt(var_X);

    } else {
        var_X = (7.787 * var_X) + (16.0d / 116.0d);

    }
    if (var_Y > 0.008856) {
        var_Y = Math.cbrt(var_Y);

    } else {
        var_Y = (7.787 * var_Y) + (16.0d / 116.0d);

    }
    if (var_Z > 0.008856) {
        var_Z = Math.cbrt(var_Z);

    } else {
        var_Z = (7.787 * var_Z) + (16.0d / 116.0d);
    }
    double CIE_L = (116 * var_Y) - 16;
    double CIE_a = 500 * (var_X - var_Y);
    double CIE_b = 200 * (var_Y - var_Z);
    returnarray[0] = CIE_L;
    returnarray[1] = CIE_a;
    returnarray[2] = CIE_b;
}

static void convertLabXYZ(double[] returnarray, double CIE_L, double CIE_a, double CIE_b) {
    double var_Y = (CIE_L + 16) / 116;
    double var_X = CIE_a / 500 + var_Y;
    double var_Z = var_Y - CIE_b / 200;

    if ((var_Y * var_Y * var_Y) > 0.008856) {
        var_Y = (var_Y * var_Y * var_Y);

    } else {
        var_Y = (((var_Y - 16) / 116)) / 7.787;
    }
    if ((var_X * var_X * var_X) > 0.008856) {
        var_X = (var_X * var_X * var_X);
    } else {
        var_X = ((var_X - 16) / 116) / 7.787;

    }
    if ((var_Z * var_Z * var_Z) > 0.008856) {
        var_Z = (var_Z * var_Z * var_Z);
    } else {
        var_Z = ((var_Z - 16) / 116) / 7.787;
    }

    double X = ref_X * var_X; //ref_X =  95.047     Observer= 2°, Illuminant= D65
    double Y = ref_Y * var_Y; //ref_Y = 100.000
    double Z = ref_Z * var_Z; //ref_Z = 108.883
    returnarray[0] = X;
    returnarray[1] = Y;
    returnarray[2] = Z;
}

static void convertXYZRGB(double[] returnarray, double X, double Y, double Z) {
    double var_X = X / 100; //X from 0 to  95.047      (Observer = 2°, Illuminant = D65)
    double var_Y = Y / 100; //Y from 0 to 100.000
    double var_Z = Z / 100; //Z from 0 to 108.883

    double var_R = (var_X * 3.2406) + (var_Y * -1.5372) + (var_Z * -0.4986);
    double var_G = (var_X * -0.9689) + (var_Y * 1.8758) + (var_Z * 0.0415);
    double var_B = (var_X * 0.0557) + (var_Y * -0.2040) + (var_Z * 1.0570);

    if (var_R > 0.0031308) {
        var_R = 1.055 * (Math.pow(var_R, (1f / 2.4f))) - 0.055;
    } else {
        var_R = 12.92 * var_R;
    }
    if (var_G > 0.0031308) {
        var_G = 1.055 * (Math.pow(var_G, (1f / 2.4f))) - 0.055;

    } else {
        var_G = 12.92 * var_G;

    }
    if (var_B > 0.0031308) {
        var_B = 1.055 * (Math.pow(var_B, (1f / 2.4f))) - 0.055;

    } else {
        var_B = 12.92 * var_B;

    }
    returnarray[0] = var_R;
    returnarray[1] = var_G;
    returnarray[2] = var_B;
}

My couple lines there do the same thing as Color.darken() here's an image of a sample color set (these colors are max distance from all previous colors through CIE-LabD2000, just using them as a robust color samples set.)

Index Color, Color.darker(), and my basic darken(), all at a FACTOR of 0.7. index vs. darker vs. darken (these should be identical)

Next for those who suggested using Lab to darken,

Index Color, Color.darker(), and Lab Darker(), all at a FACTOR of 0.7. enter image description here (is this an improvement worth the time suck?)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNickView Question on Stackoverflow
Solution 1 - JavaTed HoppView Answer on Stackoverflow
Solution 2 - JavaSileriaView Answer on Stackoverflow
Solution 3 - JavaJared RummlerView Answer on Stackoverflow
Solution 4 - JavaTatarizeView Answer on Stackoverflow