64

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?

TylerH
  • 20,799
  • 66
  • 75
  • 101
Nick
  • 6,900
  • 5
  • 45
  • 66

4 Answers4

208

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.

Ted Hopp
  • 232,168
  • 48
  • 399
  • 521
  • 1
    I find that this effect is visually much stronger on some colors than others. Therefore it wasn't very useful to me. – Reinstate Monica Mar 05 '15 at 15:50
  • 2
    @ABoschman - That's an interesting point. You might have better results by converting to [lab color space](http://en.wikipedia.org/wiki/Lab_color_space) and doing the lightening/darkening there by applying the technique of my answer to the lightness component. Unfortunately, the conversions between RGB and lab color space are not so simple (as the article discusses) and you'll have to roll your own conversion code. The advantage of lab color space is that (as discussed [here](http://en.wikipedia.org/wiki/Color_difference)) it more closely models human color perception than other spaces. – Ted Hopp Mar 05 '15 at 17:32
  • @TedHopp Wouldn't it make sense to alter HSL instead of HSV? One can use android.support.v4.graphics.ColorUtils#RGBToHSL(). – Martin Rajniak Jan 08 '16 at 15:31
  • 1
    @MartinRajniak - I don't think there's any advantage of one over the other. See, for instance [this thread](http://stackoverflow.com/questions/11396782/is-hsl-superior-over-hsi-and-hsv) or the Wikipedia article [HSL and HSV](https://en.wikipedia.org/wiki/HSL_and_HSV). I take it back -- a (very slight) advantage of HSV is that it does not depend on the support library (in case it wasn't required for other reasons). – Ted Hopp Jan 08 '16 at 16:08
  • Excellent answer, helped both in darkening and lightening a color. I would just add that you can also manipulate the `hsv[1]` parameter to get the "lightness" of the color, kind of a pastel/washed out color which I personally was going for – ZooS Jan 19 '17 at 12:41
35

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 ) );
}
Sileria
  • 15,223
  • 4
  • 49
  • 28
27

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);
}
Jared Rummler
  • 37,824
  • 19
  • 133
  • 148
  • 4
    I'm curious how my suggestion for lightening a color was failing for you. Can you provide details? – Ted Hopp Feb 05 '15 at 00:46
8

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

Tatarize
  • 10,238
  • 4
  • 58
  • 64