Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 52b2f160 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Tonal palette gradient types"

parents 28789772 7aaa353c
Loading
Loading
Loading
Loading
+113 −44
Original line number Diff line number Diff line
@@ -19,9 +19,10 @@ package com.google.android.colorextraction;
import android.app.WallpaperColors;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Color;
import android.support.v4.graphics.ColorUtils;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.SparseArray;

import com.google.android.colorextraction.types.ExtractionType;
import com.google.android.colorextraction.types.Tonal;
@@ -32,81 +33,143 @@ import java.util.ArrayList;
 * Class to process wallpaper colors and generate a tonal palette based on them.
 */
public class ColorExtractor implements WallpaperManager.OnColorsChangedListener {

    public static final int TYPE_NORMAL = 0;
    public static final int TYPE_DARK = 1;
    public static final int TYPE_EXTRA_DARK = 2;
    private static final int[] sGradientTypes = new int[]{TYPE_NORMAL, TYPE_DARK, TYPE_EXTRA_DARK};

    private static final String TAG = "ColorExtractor";
    private static final int FALLBACK_COLOR = Color.BLACK;
    private static final float DARK_TEXT_LUMINOSITY = 0.7f;

    @VisibleForTesting
    static final int FALLBACK_COLOR = 0xff83888d;

    private int mMainFallbackColor = FALLBACK_COLOR;
    private int mSecondaryFallbackColor = FALLBACK_COLOR;
    private final GradientColors mSystemColors;
    private final GradientColors mLockColors;
    private final SparseArray<GradientColors[]> mGradientColors;
    private final ArrayList<OnColorsChangedListener> mOnColorsChangedListeners;
    // Colors to return when the wallpaper isn't visible
    private final GradientColors mWpHiddenColors;
    private final Context mContext;
    private final ExtractionType mExtractionType;
    private final ArrayList<OnColorsChangedListener> mOnColorsChangedListeners;

    public ColorExtractor(Context context) {
        this(context, new Tonal());
    }

    @VisibleForTesting
    public ColorExtractor(Context context, ExtractionType extractionType) {
        mContext = context;
        mSystemColors = new GradientColors();
        mLockColors = new GradientColors();
        mExtractionType = new Tonal();
        mWpHiddenColors = new GradientColors();
        mWpHiddenColors.setMainColor(FALLBACK_COLOR);
        mWpHiddenColors.setSecondaryColor(FALLBACK_COLOR);
        mExtractionType = extractionType;

        mGradientColors = new SparseArray<>();
        for (int which : new int[] { WallpaperManager.FLAG_LOCK, WallpaperManager.FLAG_SYSTEM}) {
            GradientColors[] colors = new GradientColors[sGradientTypes.length];
            mGradientColors.append(which, colors);
            for (int type : sGradientTypes) {
                colors[type] = new GradientColors();
            }
        }

        mOnColorsChangedListeners = new ArrayList<>();

        WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);

        if (wallpaperManager == null) {
            Log.w(TAG, "Can't listen to color changes!");
        } else {
            wallpaperManager.addOnColorsChangedListener(this);

            // Initialize all gradients with the current colors
            GradientColors[] systemColors = mGradientColors.get(WallpaperManager.FLAG_SYSTEM);
            extractInto(wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM),
                    mSystemColors);
                    systemColors[TYPE_NORMAL],
                    systemColors[TYPE_DARK],
                    systemColors[TYPE_EXTRA_DARK]);

            GradientColors[] lockColors = mGradientColors.get(WallpaperManager.FLAG_LOCK);
            extractInto(wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK),
                    mLockColors);
                    lockColors[TYPE_NORMAL],
                    lockColors[TYPE_DARK],
                    lockColors[TYPE_EXTRA_DARK]);
        }
    }

    /**
     * Retrieve TYPE_NORMAL gradient colors considering wallpaper visibility.
     *
     * @param which FLAG_LOCK or FLAG_SYSTEM
     * @return colors
     */
    @NonNull
    public GradientColors getColors(int which) {
        if (which == WallpaperManager.FLAG_LOCK) {
            return mLockColors;
        } else if (which == WallpaperManager.FLAG_SYSTEM) {
            return mSystemColors;
        } else {
            throw new IllegalArgumentException("which should be either FLAG_SYSTEM or FLAG_LOCK");
        return getColors(which, TYPE_NORMAL);
    }

    /**
     * Get current gradient colors for one of the possible gradient types
     *
     * @param which FLAG_LOCK or FLAG_SYSTEM
     * @param type TYPE_NORMAL, TYPE_DARK or TYPE_EXTRA_DARK
     * @return colors
     */
    public GradientColors getColors(int which, int type) {
        if (type != TYPE_NORMAL && type != TYPE_DARK && type != TYPE_EXTRA_DARK) {
            throw new IllegalArgumentException(
                    "type should be TYPE_NORMAL, TYPE_DARK or TYPE_EXTRA_DARK");
        }
        if (which != WallpaperManager.FLAG_LOCK && which != WallpaperManager.FLAG_SYSTEM) {
            throw new IllegalArgumentException("which should be FLAG_SYSTEM or FLAG_NORMAL");
        }

        return mGradientColors.get(which)[type];
    }

    @Override
    public void onColorsChanged(WallpaperColors colors, int which) {
        boolean changed = false;
        if ((which & WallpaperManager.FLAG_LOCK) != 0) {
            extractInto(colors, mLockColors);
            for (OnColorsChangedListener listener : mOnColorsChangedListeners) {
                listener.onColorsChanged(mLockColors, WallpaperManager.FLAG_LOCK);
            }
            GradientColors[] lockColors = mGradientColors.get(WallpaperManager.FLAG_LOCK);
            extractInto(colors, lockColors[TYPE_NORMAL], lockColors[TYPE_DARK],
                    lockColors[TYPE_EXTRA_DARK]);

            changed = true;
        }
        if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
            extractInto(colors, mSystemColors);
            for (OnColorsChangedListener listener : mOnColorsChangedListeners) {
                listener.onColorsChanged(mSystemColors, WallpaperManager.FLAG_SYSTEM);
            GradientColors[] systemColors = mGradientColors.get(WallpaperManager.FLAG_SYSTEM);
            extractInto(colors, systemColors[TYPE_NORMAL], systemColors[TYPE_DARK],
                    systemColors[TYPE_EXTRA_DARK]);
            changed = true;
        }

        if (changed) {
            triggerColorsChanged(which);
        }
    }

    private void extractInto(WallpaperColors inWallpaperColors, GradientColors outGradientColors) {
        applyFallback(outGradientColors);
    private void triggerColorsChanged(int which) {
        for (OnColorsChangedListener listener: mOnColorsChangedListeners) {
            listener.onColorsChanged(this, which);
        }
    }

    private void extractInto(WallpaperColors inWallpaperColors,
            GradientColors outGradientColorsNormal, GradientColors outGradientColorsDark,
            GradientColors outGradientColorsExtraDark) {
        if (inWallpaperColors == null) {
            applyFallback(outGradientColorsNormal);
            applyFallback(outGradientColorsDark);
            applyFallback(outGradientColorsExtraDark);
            return;
        }
        boolean success = mExtractionType.extractInto(inWallpaperColors, outGradientColors);
        if (success) {
            // Updating dark text support. We're going to verify if the mean luminosity
            // is greater then a threshold.
            float hsl[] = new float[3];
            float meanLuminosity = 0;
            ColorUtils.colorToHSL(outGradientColors.getMainColor(), hsl);
            meanLuminosity += hsl[2];
            ColorUtils.colorToHSL(outGradientColors.getSecondaryColor(), hsl);
            meanLuminosity += hsl[2];
            meanLuminosity /= 2;
            outGradientColors.setSupportsDarkText(meanLuminosity >= DARK_TEXT_LUMINOSITY);
        boolean success = mExtractionType.extractInto(inWallpaperColors, outGradientColorsNormal,
                outGradientColorsDark, outGradientColorsExtraDark);
        if (!success) {
            applyFallback(outGradientColorsNormal);
            applyFallback(outGradientColorsDark);
            applyFallback(outGradientColorsExtraDark);
        }
    }

@@ -123,11 +186,11 @@ public class ColorExtractor implements WallpaperManager.OnColorsChangedListener
        }
    }

    public void addOnColorsChangedListener(OnColorsChangedListener listener) {
    public void addOnColorsChangedListener(@NonNull OnColorsChangedListener listener) {
        mOnColorsChangedListeners.add(listener);
    }

    public void removeOnColorsChangedListener(OnColorsChangedListener listener) {
    public void removeOnColorsChangedListener(@NonNull OnColorsChangedListener listener) {
        mOnColorsChangedListeners.remove(listener);
    }

@@ -184,9 +247,15 @@ public class ColorExtractor implements WallpaperManager.OnColorsChangedListener
            code = 31 * code + (mSupportsDarkText ? 0 : 1);
            return code;
        }

        @Override
        public String toString() {
            return "GradientColors(" + Integer.toHexString(mMainColor) + ", "
                    + Integer.toHexString(mSecondaryColor) + ")";
        }
    }

    public interface OnColorsChangedListener {
        void onColorsChanged(GradientColors colors, int which);
        void onColorsChanged(ColorExtractor colorExtractor, int which);
    }
}
+12 −3
Original line number Diff line number Diff line
@@ -29,10 +29,19 @@ public interface ExtractionType {
     * Executes color extraction by reading WallpaperColors and setting
     * main and secondary colors on GradientColors.
     *
     * Extraction is expected to happen with 3 different gradient types:
     *     Normal, with the main extracted colors
     *     Dark, with extra contrast
     *     ExtraDark, for places where GAR is mandatory, like the emergency dialer
     *
     * @param inWallpaperColors where to read from
     * @param outGradientColors object that should receive the colors
     * @return true if successful
     * @param outGradientColorsNormal object that should receive normal colors
     * @param outGradientColorsDark object that should receive dark colors
     * @param outGradientColorsExtraDark object that should receive extra dark colors
     * @return true if successful.
     */
    boolean extractInto(WallpaperColors inWallpaperColors,
            ColorExtractor.GradientColors outGradientColors);
            ColorExtractor.GradientColors outGradientColorsNormal,
            ColorExtractor.GradientColors outGradientColorsDark,
            ColorExtractor.GradientColors outGradientColorsExtraDark);
}
+81 −43
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ import android.util.MathUtils;
import android.util.Pair;
import android.util.Range;

import com.google.android.colorextraction.ColorExtractor;
import com.google.android.colorextraction.ColorExtractor.GradientColors;

/**
 * Implementation of tonal color extraction
@@ -43,25 +43,25 @@ public class Tonal implements ExtractionType {
    // When extracting the main color, only consider colors
    // present in at least MIN_COLOR_OCCURRENCE of the image
    private static final float MIN_COLOR_OCCURRENCE = 0.1f;
    private static final boolean DEBUG = true;

    // Secondary color will be darker than the main color when
    // main color is brighter than this variable.
    private static final float MAX_COLOR_LUMINOSITY = 0.8f;
    // Luminosity difference between main and secondary colors
    // should never be greater then this.
    private static final float MAX_LUMINOSITY_DISTANCE = 0.35f;
    // Temporary variable to avoid allocations
    private float[] mTmpHSL = new float[3];

    /**
     * Grab colors from WallpaperColors as set them into GradientColors
     *
     * @param wallpaperColors input
     * @param gradientColors output
     * @param inWallpaperColors input
     * @param outColorsNormal colors for normal theme
     * @param outColorsDark colors for dar theme
     * @param outColorsExtraDark colors for extra dark theme
     * @return true if successful
     */
    public boolean extractInto(WallpaperColors wallpaperColors,
            ColorExtractor.GradientColors gradientColors) {
    public boolean extractInto(@NonNull WallpaperColors inWallpaperColors,
            @NonNull GradientColors outColorsNormal, @NonNull GradientColors outColorsDark,
            @NonNull GradientColors outColorsExtraDark) {

        if (wallpaperColors.getColors().size() == 0) {
        if (inWallpaperColors.getColors().size() == 0) {
            return false;
        }
        // Tonal is not really a sort, it takes a color from the extracted
@@ -70,17 +70,17 @@ public class Tonal implements ExtractionType {
        // and replaces the original palette

        // First find the most representative color in the image
        populationSort(wallpaperColors);
        populationSort(inWallpaperColors);
        // Calculate total
        int total = 0;
        for (Pair<Color, Integer> weightedColor : wallpaperColors.getColors()) {
        for (Pair<Color, Integer> weightedColor : inWallpaperColors.getColors()) {
            total += weightedColor.second;
        }

        // Get bright colors that occur often enough in this image
        Pair<Color, Integer> bestColor = null;
        float[] hsl = new float[3];
        for (Pair<Color, Integer> weightedColor : wallpaperColors.getColors()) {
        for (Pair<Color, Integer> weightedColor : inWallpaperColors.getColors()) {
            float colorOccurrence = weightedColor.second / (float) total;
            if (colorOccurrence < MIN_COLOR_OCCURRENCE) {
                break;
@@ -97,7 +97,7 @@ public class Tonal implements ExtractionType {
            }
        }

        // Fallback to first color
        // Fail if not found
        if (bestColor == null) {
            return false;
        }
@@ -107,59 +107,97 @@ public class Tonal implements ExtractionType {
                hsl);

        // The Android HSL definition requires the hue to go from 0 to 360 but
        // the Material Tonal Palette defines hues from 0 to 1
        hsl[0] /= 360.0f; // normalize
        // the Material Tonal Palette defines hues from 0 to 1.
        hsl[0] /= 360f;

        // Find the palette that contains the closest color
        TonalPalette palette = findTonalPalette(hsl[0]);

        if (palette == null) {
            Log.w(TAG, "Could not find a tonal palette!");
            return false;
        }

        // Figure out what's the main color index in the optimal palette
        int fitIndex = bestFit(palette, hsl[0], hsl[1], hsl[2]);
        if (fitIndex == -1) {
            Log.w(TAG, "Could not find best fit!");
            return false;
        }

        // Generate the 10 colors palette by offsetting each one of them
        float[] h = fit(palette.h, hsl[0], fitIndex,
                Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
        float[] s = fit(palette.s, hsl[1], fitIndex, 0.0f, 1.0f);
        float[] l = fit(palette.l, hsl[2], fitIndex, 0.0f, 1.0f);

        hsl[0] = fract(h[fitIndex]) * 360.0f;
        hsl[1] = s[fitIndex];
        hsl[2] = l[fitIndex];
        gradientColors.setMainColor(ColorUtils.HSLToColor(hsl));
        final int textInversionIndex = h.length - 3;
        if (DEBUG) {
            StringBuilder builder = new StringBuilder("Tonal Palette - index: " + fitIndex +
                    ". Main color: " + Integer.toHexString(getColorInt(fitIndex, h, s, l)) +
                    "\nColors: ");

        int secondColorIndex = fitIndex;
        if (hsl[2] > MAX_COLOR_LUMINOSITY) {
            for (int i = secondColorIndex - 1; i >= 0; i--) {
                float distance = Math.abs(hsl[2] - l[i]);
                if (distance > MAX_LUMINOSITY_DISTANCE) {
                    break;
            for (int i=0; i < h.length; i++) {
                builder.append(Integer.toHexString(getColorInt(i, h, s, l)));
                if (i < h.length - 1) {
                    builder.append(", ");
                }
                secondColorIndex = i;
            }
        } else {
            for (int i = secondColorIndex + 1; i < h.length; i++) {
                float distance = Math.abs(hsl[2] - l[i]);
                if (distance > MAX_LUMINOSITY_DISTANCE) {
                    break;
                }
                secondColorIndex = i;
            Log.d(TAG, builder.toString());
        }

        // Normal colors:
        // best fit + a 2 colors offset
        int primaryIndex = fitIndex;
        int secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
        outColorsNormal.setMainColor(getColorInt(primaryIndex, h, s, l));
        outColorsNormal.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));

        // Dark colors:
        // Stops at 4th color, only lighter if dark text is supported
        if (fitIndex < 2) {
            primaryIndex = 0;
        } else if (fitIndex < textInversionIndex) {
            primaryIndex = Math.min(fitIndex, 3);
        } else {
            primaryIndex = h.length - 1;
        }
        secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
        outColorsDark.setMainColor(getColorInt(primaryIndex, h, s, l));
        outColorsDark.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));

        // Extra Dark:
        // Stay close to dark colors until dark text is supported
        if (fitIndex < 2) {
            primaryIndex = 0;
        } else if (fitIndex < textInversionIndex) {
            primaryIndex = 2;
        } else {
            primaryIndex = h.length - 1;
        }
        secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
        outColorsExtraDark.setMainColor(getColorInt(primaryIndex, h, s, l));
        outColorsExtraDark.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));

        hsl[0] = fract(h[secondColorIndex]) * 360.0f;
        hsl[1] = s[secondColorIndex];
        hsl[2] = l[secondColorIndex];
        gradientColors.setSecondaryColor(ColorUtils.HSLToColor(hsl));
        final boolean supportsDarkText = fitIndex >= textInversionIndex;
        outColorsNormal.setSupportsDarkText(supportsDarkText);
        outColorsDark.setSupportsDarkText(supportsDarkText);
        outColorsExtraDark.setSupportsDarkText(supportsDarkText);

        if (DEBUG) {
            Log.d(TAG, "Gradients: \n\tNormal " + outColorsNormal + "\n\tDark " + outColorsDark
            + "\n\tExtra dark: " + outColorsExtraDark);
        }

        return true;
    }

    private int getColorInt(int fitIndex, float[] h, float[] s, float[] l) {
        mTmpHSL[0] = fract(h[fitIndex]) * 360.0f;
        mTmpHSL[1] = s[fitIndex];
        mTmpHSL[2] = l[fitIndex];
        return ColorUtils.HSLToColor(mTmpHSL);
    }

    /**
     * Checks if a given color exists in the blacklist
     * @param hsl float array with 3 components (H 0..360, S 0..1 and L 0..1)
+114 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */
package com.google.android.colorextraction;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.WallpaperColors;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Color;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;

import com.google.android.colorextraction.ColorExtractor.GradientColors;
import com.google.android.colorextraction.types.ExtractionType;
import com.google.android.colorextraction.types.Tonal;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Tests tonal palette generation.
 */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ColorExtractorTest {

    Context mContext;

    @Before
    public void setup() {
        mContext = InstrumentationRegistry.getContext();
    }

    @Test
    public void ColorExtractor_extractWhenInitialized() {
        ExtractionType type = mock(Tonal.class);
        new ColorExtractor(mContext, type);
        // 1 for lock and 1 for system
        verify(type, times(2))
                .extractInto(any(), any(), any(), any());
    }

    @Test
    public void getColors_usesFallbackIfFails() {
        ExtractionType alwaysFail =
                (inWallpaperColors, outGradientColorsNormal, outGradientColorsDark,
                        outGradientColorsExtraDark) -> false;
        ColorExtractor extractor = new ColorExtractor(mContext, alwaysFail);
        GradientColors colors = extractor.getColors(WallpaperManager.FLAG_SYSTEM);

        assertEquals("Should be using the fallback color.",
                colors.getMainColor(), ColorExtractor.FALLBACK_COLOR);
        assertEquals("Should be using the fallback color.",
                colors.getSecondaryColor(), ColorExtractor.FALLBACK_COLOR);
        assertFalse("Dark text support should be false.", colors.supportsDarkText());
    }

    @Test
    public void getColors_usesExtractedColors() {
        GradientColors colorsExpectedNormal = new GradientColors();
        colorsExpectedNormal.setMainColor(Color.RED);
        colorsExpectedNormal.setSecondaryColor(Color.GRAY);

        GradientColors colorsExpectedDark = new GradientColors();
        colorsExpectedNormal.setMainColor(Color.BLACK);
        colorsExpectedNormal.setSecondaryColor(Color.BLUE);

        GradientColors colorsExpectedExtraDark = new GradientColors();
        colorsExpectedNormal.setMainColor(Color.MAGENTA);
        colorsExpectedNormal.setSecondaryColor(Color.GREEN);

        ExtractionType type =
                (inWallpaperColors, outGradientColorsNormal, outGradientColorsDark,
                        outGradientColorsExtraDark) -> {
            outGradientColorsNormal.set(colorsExpectedNormal);
            outGradientColorsDark.set(colorsExpectedDark);
            outGradientColorsExtraDark.set(colorsExpectedExtraDark);
            // Successful extraction
            return true;
        };
        ColorExtractor extractor = new ColorExtractor(mContext, type);

        assertEquals("Extracted colors not being used!",
                extractor.getColors(WallpaperManager.FLAG_SYSTEM, ColorExtractor.TYPE_NORMAL),
                colorsExpectedNormal);
        assertEquals("Extracted colors not being used!",
                extractor.getColors(WallpaperManager.FLAG_SYSTEM, ColorExtractor.TYPE_DARK),
                colorsExpectedDark);
        assertEquals("Extracted colors not being used!",
                extractor.getColors(WallpaperManager.FLAG_SYSTEM, ColorExtractor.TYPE_EXTRA_DARK),
                colorsExpectedExtraDark);
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.util.Pair;
import android.util.Range;

import com.google.android.colorextraction.ColorExtractor;
import com.google.android.colorextraction.ColorExtractor.GradientColors;

import org.junit.Test;
import org.junit.runner.RunWith;
@@ -76,7 +77,8 @@ public class TonalTest {

        // Make sure that palette generation will fail
        Tonal tonal = new Tonal();
        boolean success = tonal.extractInto(colors, new ColorExtractor.GradientColors());
        boolean success = tonal.extractInto(colors, new GradientColors(), new GradientColors(),
                new GradientColors());
        assertFalse("Cannot generate a tonal palette from blacklisted colors ", success);
    }
}
Loading