Android Jetpack: Use Palette for picture color picking

Things related to product MM

There is a new product MM, because it is relatively flat, let's call her A sister. Sister A pointed out on the first day that the background of the banner at the top of the page is white, which is too monotonous, and people don’t like it. The background color needs to be automatically switched according to the content of the advertisement picture, and the color should be consistent with the main color of the advertisement picture. As a qualified code farmer, I directly refused. I said that our application focuses on simplicity. Sister A didn't give up either, she had an in-depth exchange with me all night and successfully convinced me.

In fact, it is not difficult to realize this requirement. Google has provided us with a convenient tool —— Palette.

foreword

The function of Palette is actually released very early. Jetpack also includes this function. If you want to use this function, you need to rely on the library first.

implementation 'androidx.palette:palette:1.0.0'

This article will explain how to use Palette to extract colors from pictures.

Create Palettes

Creating a Palette is actually very simple, as follows

var builder = Palette.from(bitmap)
var palette = builder.generate()

In this way, we create a Pallete object through a Bitmap.

Note: Palette.generate(bitmap)It is also possible to use it directly, but this method is no longer recommended, and many old articles on the Internet still use this method. It is recommended to use Palette.Builder in this way.

generate()This function is synchronous. Of course, considering that image processing may be time-consuming, Android also provides asynchronous functions.

public AsyncTask<Bitmap, Void, Palette> generate(
        @NonNull final PaletteAsyncListener listener) {
    
    

Get the Palette instance through a PaletteAsyncListener, the interface is as follows:

public interface PaletteAsyncListener {
    
    
    /**
     * Called when the {@link Palette} has been generated. {@code null} will be passed when an
     * error occurred during generation.
     */
    void onGenerated(@Nullable Palette palette);
}

extract color

With a Palette instance, you can get the color in the picture through the corresponding function of the Palette object, and there is more than one color, listed below:

  • getDominantColor: Get the dominant color in the picture
  • getMutedColor: Get the muted color in the picture
  • getDarkMutedColor: Get the soft dark color in the picture
  • getLightMutedColor: Get the soft bright color in the picture
  • getVibrantColor: Get the vibrant color in the picture
  • getDarkVibrantColor: Get the vibrant dark color in the picture
  • getLightVibrantColor: Get the vibrant bright color in the picture

These functions all need to provide a default color, if the color Swatch is invalid, use this default color. It’s not intuitive to say so, let’s test it, the code is as follows:

var bitmap = BitmapFactory.decodeResource(resources, R.mipmap.a)
var builder = Palette.from(bitmap)
var palette = builder.generate()
color0.setBackgroundColor(palette.getDominantColor(Color.WHITE))
color1.setBackgroundColor(palette.getMutedColor(Color.WHITE))
color2.setBackgroundColor(palette.getDarkMutedColor(Color.WHITE))
color3.setBackgroundColor(palette.getLightMutedColor(Color.WHITE))
color4.setBackgroundColor(palette.getVibrantColor(Color.WHITE))
color5.setBackgroundColor(palette.getDarkVibrantColor(Color.WHITE))
color6.setBackgroundColor(palette.getLightVibrantColor(Color.WHITE))

The result after running is as follows:

111.jpg

In this way, the difference of each color is clear at a glance. In addition to the above functions, you can also use getColorForTargetthis function as follows:

@ColorInt
public int getColorForTarget(@NonNull final Target target, @ColorInt final int defaultColor) {
    
    

This function requires a Target and provides 6 static fields, as follows:

/**
 * A target which has the characteristics of a vibrant color which is light in luminance.
*/
public static final Target LIGHT_VIBRANT;

/**
 * A target which has the characteristics of a vibrant color which is neither light or dark.
 */
public static final Target VIBRANT;

/**
 * A target which has the characteristics of a vibrant color which is dark in luminance.
 */
public static final Target DARK_VIBRANT;

/**
 * A target which has the characteristics of a muted color which is light in luminance.
 */
public static final Target LIGHT_MUTED;

/**
 * A target which has the characteristics of a muted color which is neither light or dark.
 */
public static final Target MUTED;

/**
 * A target which has the characteristics of a muted color which is dark in luminance.
 */
public static final Target DARK_MUTED;

In fact, it corresponds to the six colors above except the main color.

Automatic text color adaptation

As can be seen from the above running results, the text on each color is clearly displayed, and they are not the same color. In fact, this is also the function provided by Palette.

Through the following functions, we can get the Swatch objects corresponding to various hues:

  • getDominantSwatch
  • getMutedSwatch
  • getDarkMutedSwatch
  • getLightMutedSwatch
  • getVibrantSwatch
  • getDarkVibrantSwatch
  • getLightVibrantSwatch

Note: As above, it can also be getSwatchForTarget(@NonNull final Target target)obtained by

The Swatch class provides the following functions:

  • getPopulation(): the number of pixels in the sample
  • getRgb(): RBG value of the color
  • getHsl(): the HSL value of the color
  • getBodyTextColor(): Can adapt to the color value of the body text of this Swatch
  • getTitleTextColor(): Can adapt to the color value of the title text of this Swatch

So we can easily get title and body text colors that can be very realistic in this color by getBodyTextColor()and . getTitleTextColor()So the above test code is complete as follows:

var bitmap = BitmapFactory.decodeResource(resources, R.mipmap.a)
var builder = Palette.from(bitmap)
var palette = builder.generate()

color0.setBackgroundColor(palette.getDominantColor(Color.WHITE))
color0.setTextColor(palette.dominantSwatch?.bodyTextColor ?: Color.WHITE)

color1.setBackgroundColor(palette.getMutedColor(Color.WHITE))
color1.setTextColor(palette.mutedSwatch?.bodyTextColor ?: Color.WHITE)

color2.setBackgroundColor(palette.getDarkMutedColor(Color.WHITE))
color2.setTextColor(palette.darkMutedSwatch?.bodyTextColor ?: Color.WHITE)

color3.setBackgroundColor(palette.getLightMutedColor(Color.WHITE))
color3.setTextColor(palette.lightMutedSwatch?.bodyTextColor ?: Color.WHITE)

color4.setBackgroundColor(palette.getVibrantColor(Color.WHITE))
color4.setTextColor(palette.vibrantSwatch?.bodyTextColor ?: Color.WHITE)

color5.setBackgroundColor(palette.getDarkVibrantColor(Color.WHITE))
color5.setTextColor(palette.darkVibrantSwatch?.bodyTextColor ?: Color.WHITE)

color6.setBackgroundColor(palette.getLightVibrantColor(Color.WHITE))
color6.setTextColor(palette.lightVibrantSwatch?.bodyTextColor ?: Color.WHITE)

In this way, the text on each color can be displayed clearly.

So what is the difference between the title and body text colors, and how did they get there? Let's take a look at the source code:

/**
 * Returns an appropriate color to use for any 'title' text which is displayed over this
 * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast.
 */
@ColorInt
public int getTitleTextColor() {
    
    
    ensureTextColorsGenerated();
    return mTitleTextColor;
}

/**
 * Returns an appropriate color to use for any 'body' text which is displayed over this
 * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast.
 */
@ColorInt
public int getBodyTextColor() {
    
    
    ensureTextColorsGenerated();
    return mBodyTextColor;
}

You can see that it will be executed first ensureTextColorsGenerated(), and its source code is as follows:

private void ensureTextColorsGenerated() {
    
    
    if (!mGeneratedTextColors) {
    
    
        // First check white, as most colors will be dark
        final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha(
                Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT);
        final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha(
                Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT);

        if (lightBodyAlpha != -1 && lightTitleAlpha != -1) {
    
    
            // If we found valid light values, use them and return
            mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha);
            mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha);
            mGeneratedTextColors = true;
            return;
        }

        final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha(
                Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT);
        final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha(
                Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT);

        if (darkBodyAlpha != -1 && darkTitleAlpha != -1) {
    
    
            // If we found valid dark values, use them and return
            mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
            mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
            mGeneratedTextColors = true;
            return;
        }

        // If we reach here then we can not find title and body values which use the same
        // lightness, we need to use mismatched values
        mBodyTextColor = lightBodyAlpha != -1
                ? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha)
                : ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
        mTitleTextColor = lightTitleAlpha != -1
                ? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha)
                : ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
        mGeneratedTextColors = true;
    }
}

As you can see from the code, these two text colors are actually either white or black, but the alpha of transparency is different.

There is a key function here, namely ColorUtils.calculateMinimumAlpha():

public static int calculateMinimumAlpha(@ColorInt int foreground, @ColorInt int background,
        float minContrastRatio) {
    
    
    if (Color.alpha(background) != 255) {
    
    
        throw new IllegalArgumentException("background can not be translucent: #"
                + Integer.toHexString(background));
    }

    // First lets check that a fully opaque foreground has sufficient contrast
    int testForeground = setAlphaComponent(foreground, 255);
    double testRatio = calculateContrast(testForeground, background);
    if (testRatio < minContrastRatio) {
    
    
        // Fully opaque foreground does not have sufficient contrast, return error
        return -1;
    }

    // Binary search to find a value with the minimum value which provides sufficient contrast
    int numIterations = 0;
    int minAlpha = 0;
    int maxAlpha = 255;

    while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS &&
            (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) {
    
    
        final int testAlpha = (minAlpha + maxAlpha) / 2;

        testForeground = setAlphaComponent(foreground, testAlpha);
        testRatio = calculateContrast(testForeground, background);

        if (testRatio < minContrastRatio) {
    
    
            minAlpha = testAlpha;
        } else {
    
    
            maxAlpha = testAlpha;
        }

        numIterations++;
    }

    // Conservatively return the max of the range of possible alphas, which is known to pass.
    return maxAlpha;
}

It calculates the most suitable Alpha for the foreground color based on the background color and the foreground color. During this period, if it is less than minContrastRatio, -1 is returned, indicating that the foreground color is not suitable. The difference between the title and the main text is that the minContrastRatio is different.

Going back to ensureTextColorsGeneratedthe code, you can see that the Alpha of the white foreground color is calculated based on the current hue. If both Alphas are not -1, the corresponding color is returned; otherwise, the Alpha of the black foreground color is calculated. If neither is -1, the corresponding color is returned. Color; otherwise, the title and body text should be white and black, and the corresponding color can be returned.

More features

When we created the Palette above, we first passed Palette.from(bitmap)a Palette.Builder object. Through this builder, more functions can be realized, such as:

  • addFilter: add a filter
  • setRegion: Set the extraction area on the picture
  • maximumColorCount: the maximum number of colors in the palette
    , etc.

Summarize

As we can see above, Palette is very powerful, but it is very simple to use, allowing us to easily extract the color in the picture and adapt the appropriate text color. At the same time, note that because ColorUtils is public, when we need to automatically adapt the color of the text, we can also use several functions of ColorUtils to realize the scheme of calculating dynamic colors by ourselves.

Jetpack provides many functional modules, such as ActivityResult for processing communication, you can refer to [Jetpack] ActivityResult introduction and principle analysis

Guess you like

Origin blog.csdn.net/chzphoenix/article/details/122967657