Loading packages/SystemUI/colorextraction/src/com/google/android/colorextraction/ColorExtractor.java +113 −44 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); } } Loading @@ -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); } Loading Loading @@ -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); } } packages/SystemUI/colorextraction/src/com/google/android/colorextraction/types/ExtractionType.java +12 −3 Original line number Diff line number Diff line Loading @@ -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); } packages/SystemUI/colorextraction/src/com/google/android/colorextraction/types/Tonal.java +81 −43 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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; Loading @@ -97,7 +97,7 @@ public class Tonal implements ExtractionType { } } // Fallback to first color // Fail if not found if (bestColor == null) { return false; } Loading @@ -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) Loading packages/SystemUI/colorextraction/tests/src/com/google/android/colorextraction/ColorExtractorTest.java 0 → 100644 +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); } } packages/SystemUI/colorextraction/tests/src/com/google/android/colorextraction/types/TonalTest.java +3 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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
packages/SystemUI/colorextraction/src/com/google/android/colorextraction/ColorExtractor.java +113 −44 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); } } Loading @@ -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); } Loading Loading @@ -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); } }
packages/SystemUI/colorextraction/src/com/google/android/colorextraction/types/ExtractionType.java +12 −3 Original line number Diff line number Diff line Loading @@ -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); }
packages/SystemUI/colorextraction/src/com/google/android/colorextraction/types/Tonal.java +81 −43 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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; Loading @@ -97,7 +97,7 @@ public class Tonal implements ExtractionType { } } // Fallback to first color // Fail if not found if (bestColor == null) { return false; } Loading @@ -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) Loading
packages/SystemUI/colorextraction/tests/src/com/google/android/colorextraction/ColorExtractorTest.java 0 → 100644 +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); } }
packages/SystemUI/colorextraction/tests/src/com/google/android/colorextraction/types/TonalTest.java +3 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } }