material design – Differences between Android Palette colors-ThrowExceptions

Exception or error:

Edit

To get a better feel for the Android Palette class, I decided to make a simple app to test some of its features – if you are interested, you can find the app on the Play Store: https://play.google.com/store/apps/details?id=com.tonyw.sampleapps.palettecolorextraction. Basically it just has images and the colors that the Palette class extracts (mentioned below), and you can add your own images to test as well. You can find my source code on Github: https://github.com/tony-w/PaletteColorExtraction

Screenshots
Some pictures with corresponding extracted colors
Another picture with extracted colors
Title and body text color extracted based on background color

Original post

Can someone describe the differences between the colors that can be extracted from a Bitmap using Android’s Palette class?

  • Vibrant
  • Vibrant Dark
  • Vibrant Light
  • Muted
  • Muted Dark
  • Muted Light

Is it just that the muted colors are duller than vibrant colors? Are dark and light supposed to better match Lollipop’s dark and light material design themes, respectively?

How to solve:

That’s a really good question. If you look at the source code you can see that the different swatches are chosen from analyzing the HSL color profile of the pixels on the image, based on target ranges for luminance, saturation, and population (how many pixels in the image are represented by the swatch). It uses a weighted average calculation with preference given to luminance, then saturation, then population.

Generally speaking, vibrant colors are more saturated than muted colors, dark colors are darker, and light swatches are lighter. Which one you use depends on the overall effect you want.

Chris Banes wrote in his blog “Vibrant and Dark Vibrant are the ones that developers will use mostly” but in practice it isn’t quite as simple as that.

One example I’ve found is the applyPalette method in Romain Guy’s sample app from Google IO 2014, although the code assumes the various swatches will be found (possibly because he’s working with known images).

Depending on the image, it’s possible that some swatch types won’t be found, so be sure to account for that possibility in your code.

For example, you could try getting swatches from the palette in a specific order, e.g. for a Dark theme you might try getting Vibrant Dark, then Muted Dark, and then fall back on some default color.

If you want something that is a bit more predictable, it is also possible to grab the most represented color, like this:

public static Palette.Swatch getDominantSwatch(Palette palette) {
    // find most-represented swatch based on population
    return Collections.max(palette.getSwatches(), new Comparator<Palette.Swatch>() {
        @Override
        public int compare(Palette.Swatch sw1, Palette.Swatch sw2) {
            return Integer.compare(sw1.getPopulation(), sw2.getPopulation());
        }
    });
}

The HSL values for each swatch are also accessible, so you could write a similar routine to choose, for example, the most saturated swatch that isn’t the dominant color.


Using custom targets

Another thing I’ve found useful is to define some custom targets in addition to the 6 targets defined in Palette.Target, with different weighting and target lightness and saturation values, in order to increase the chance of finding a useful color.

For example, you can ask Palette’s quantizer to include a swatch for the most dominant color that meets the current filter criteria with a target like this:

public static final Target DOMINANT;

static {
    DOMINANT = new Target.Builder().setPopulationWeight(1f)
                                   .setSaturationWeight(0f)
                                   .setLightnessWeight(0f)
                                   .setExclusive(false)
                                   .build();
}

And you can get some useful swatches that put a higher priority on lightness than the stock targets like this:

public static final Target DARK;
public static final Target LIGHT;
public static final Target NEUTRAL;

static {
    DARK = new Target.Builder().setMinimumLightness(0f)
                               .setTargetLightness(0.26f)
                               .setMaximumLightness(0.5f)
                               .setMinimumSaturation(0.1f)
                               .setTargetSaturation(0.6f)
                               .setMaximumSaturation(1f)
                               .setPopulationWeight(0.18f)
                               .setSaturationWeight(0.22f)
                               .setLightnessWeight(0.60f)
                               .setExclusive(false)
                               .build();

    LIGHT = new Target.Builder().setMinimumLightness(0.50f)
                                .setTargetLightness(0.74f)
                                .setMaximumLightness(1.0f)
                                .setMinimumSaturation(0.1f)
                                .setTargetSaturation(0.7f)
                                .setMaximumSaturation(1f)
                                .setPopulationWeight(0.18f)
                                .setSaturationWeight(0.22f)
                                .setLightnessWeight(0.60f)
                                .setExclusive(false)
                                .build();

    NEUTRAL = new Target.Builder().setMinimumLightness(0.20f)
                                  .setTargetLightness(0.5f)
                                  .setMaximumLightness(0.8f)
                                  .setMinimumSaturation(0.1f)
                                  .setTargetSaturation(0.6f)
                                  .setMaximumSaturation(1f)
                                  .setPopulationWeight(0.18f)
                                  .setSaturationWeight(0.22f)
                                  .setLightnessWeight(0.60f)
                                  .setExclusive(false)
                                  .build();
}

You can access the swatch found for a custom target after it is generated by using Palette.getSwatchForTarget, for example:

Palette.Swatch neutral = Palette.getSwatchForTarget(NEUTRAL);

Be aware of the default filter

By default Palette has a Palette.Filter that rejects colors very near to black or white, as well as colors “very near to the red I line” which I believe is referring to isochronous colors that are difficult for people with red-blind color blindness.

My theory is that it rejects the near-white and near-black colors in order to help prevent those from being chosen as “saturated” colors when the saturation is weighted highly.

The result of this filter however is that no swatch will be found for images that consist entirely of almost white and/or almost black pixels, as well as a tendency to avoid choosing colors with a pinkish hue.

However, it is possible to remove this filter using Palette.Builder.clearFilters(), and add your own filter with Palette.Builder.addFilter().

In my code I’ve chosen to make a second attempt at generating a palette if the first attempt doesn’t return any swatches:

Palette palette = new Palette.Builder(bitmap).addTarget(DOMINANT)
                                             .addTarget(DARK)
                                             .addTarget(LIGHT)
                                             .addTarget(NEUTRAL)
                                             .generate();
if(palette.getSwatches().isEmpty()) {
    Log.v(TAG, "Getting alternate (UNFILTERED) palette.");
    palette = new Palette.Builder(bitmap).addTarget(DOMINANT)
                                         .addTarget(DARK)
                                         .addTarget(LIGHT)
                                         .addTarget(NEUTRAL)
                                         .clearFilters() /// allow isBlack(), isWhite(), isNearRedILine()
                                         .generate();
}

Chaining the attempts preserves the usefulness of the default filter in most cases, but still allows you to find swatches for images that the default would completely reject.

Leave a Reply

Your email address will not be published. Required fields are marked *