Android's media style notification

Android’s media style notification

I was recently writing an Audiobook app and wanted to get dynamic colors based on the book cover similar to Android’s media style notifications. Obviously the best method is to use the Palette API, but implementing it for the most aesthetically pleasing colors with good contrast wasn’t immediately clear.

Phonograph Screenshot

Phonograph’s colors with white buttons and text

Some popular apps like Pocketcasts for Phonograph seem to get the most vibrant color they can find from the art and then darken it so the white buttons and text will have enough contrast. I used this method for previous apps since it was easy but this method bothered me since it was possible that the primary color wouldn’t match very well and often clashed with the notification.

To better match Android’s notifications we need to also fetch a complementary color from the art that has enough contrast for good readability.

To start, let’s generate a palette:

public static Palette generatePalette(Bitmap bitmap) {
    if (bitmap == null) return null;
    Palette palette = new Palette.Builder(bitmap)
            .generate();
    if(palette.getSwatches().size() <= 1) {
        palette = new Palette.Builder(bitmap)
                .clearFilters()
                .generate();
    }
    return palette;
}

There are a couple things to note here. By default, the palette builder has filters that disable colors that are close to black or white or certain hues that might cause accessibility issues for color blind folks like me. The second part in green will clear all filters in case these are the only colors that can be found. You can even add your own filters using addFilter.

Next, we need to be able to easily figure out the distance between two LAB colors. We can do this easily with ColorUtils.

private static double calculateDistance(int color0, int color1) {
    double[] lab0 = new double[3];
    double[] lab1 = new double[3];

    ColorUtils.colorToLAB(color0, lab0);
    ColorUtils.colorToLAB(color1, lab1);

    return ColorUtils.distanceEuclidean(lab0, lab1);
}

Now we need a way to compare different swatches to find the swatch with the greatest distance.

private static class distanceComparator implements Comparator<Palette.Swatch> {
    private int color;
    private distanceComparator(int color) {
        this.color = color;
    }

    @Override
    public int compare(Palette.Swatch swatch1, Palette.Swatch swatch2) {
        return (int)(calculateDistance(swatch2.getRgb(), color)
                - calculateDistance(swatch1.getRgb(), color));
    }
}

Okay. Now everything is set up to get the colors.

public static int[] getMatchingColors(Palette palette, int[]fallback, boolean invert) {
    if (palette != null) {
        if(palette.getDominantSwatch() != null) {
            Palette.Swatch swatch = palette.getDominantSwatch();
            int swatchColor = swatch.getRgb();
            int matchingColor = -1;
            List<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());
            
            Collections.sort(swatches, new distanceComparator(swatchColor));
            for(Palette.Swatch contrastingSwatch : swatches) {
                Cat.d("Testing contrast to " + ColorUtils.calculateContrast(contrastingSwatch.getRgb(), swatchColor));
                if(ColorUtils.calculateContrast(contrastingSwatch.getRgb(), swatchColor) > MIN_ALPHA_CONTRAST) {
                    matchingColor = swatches.get(0).getRgb();
                    break;
                }
            }
            if(matchingColor == -1)
                matchingColor = swatch.getBodyTextColor();

            if(invert)
                return new int[]{matchingColor, swatchColor};
            return new int[]{swatchColor, matchingColor};
        }
    }
    return fallback;
}

Some explanation here. getDominantSwatch() gets the swatch generated that appears the most in the bitmap and is close to what notifications use for the background.

Next we can get a list of all the swatches that were generated in the palette and use the comparator we made earlier to sort them by the greatest distance. We can then loop through them making sure the swatch also has a high enough contrast for readability. More information can be found in the Material guidelines for this.

If no matching color can be found, we can use getBodyTextColor() to get a guaranteed contrasting color. (this will either be white or black with some alpha)

I also added an invert option to my function in case a user wants to invert the colors for their audiobook to make it lighter or darker.

NavBooks Screenshot

The completed result in NavBooks.

The result gives you colors that are generally very close to the notification, but may still not match up perfectly. I think there are a couple things that may factor into the difference such as the bitmap being resized. I suspect the notification may also be getting the dominant color from the left side of the image for the  gradient it uses. You can try to get these to match better if you want, but I think the colors generated with this method will get as good results.

Do you have any other methods for generating colors that you use?