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

Commit 7aaa353c authored by Lucas Dupin's avatar Lucas Dupin
Browse files

Tonal palette gradient types

The tonal palette now has 3 gradient types, one for regular scrims
another one for places that require stronger contrast, and a last
version for extra contrast.

Bug: 62161354
Test: runtest -x frameworks/base/packages/SystemUI/colorextraction/tests/src/com/google/android/colorextraction/ColorExtractorTest.java
Change-Id: I0be6726334f7a71f04ee02c61994101c97771f1a
parent b1d93c61
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