diff options
author | Mario Bertschler <bmario@google.com> | 2017-05-03 10:03:30 -0700 |
---|---|---|
committer | Mario Bertschler <bmario@google.com> | 2017-05-08 16:22:27 -0700 |
commit | 1d6300bf664c5e56f904c4e722bab04f0e3c2c6b (patch) | |
tree | b792d29583f0343d94f86d7f58de31d2fd9261e0 /src/com/android/launcher3/dynamicui | |
parent | 5c60e7148774ae6e75ea44ea49156c78a542592e (diff) | |
download | android_packages_apps_Trebuchet-1d6300bf664c5e56f904c4e722bab04f0e3c2c6b.tar.gz android_packages_apps_Trebuchet-1d6300bf664c5e56f904c4e722bab04f0e3c2c6b.tar.bz2 android_packages_apps_Trebuchet-1d6300bf664c5e56f904c4e722bab04f0e3c2c6b.zip |
Color extraction implementation for gradient colors
used by all apps background.
Change-Id: Id7a652815cb3ef61ff4b0a442d8350337014b56d
Diffstat (limited to 'src/com/android/launcher3/dynamicui')
6 files changed, 544 insertions, 13 deletions
diff --git a/src/com/android/launcher3/dynamicui/ColorExtractionService.java b/src/com/android/launcher3/dynamicui/ColorExtractionService.java index f6b02aa9c..349b4fff6 100644 --- a/src/com/android/launcher3/dynamicui/ColorExtractionService.java +++ b/src/com/android/launcher3/dynamicui/ColorExtractionService.java @@ -66,7 +66,7 @@ public class ColorExtractionService extends IntentService { if (FeatureFlags.QSB_IN_HOTSEAT || FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) { extractedColors.updateWallpaperThemePalette(null); if (FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) { - extractedColors.updateAllAppsGradientPalette(null); + extractedColors.updateAllAppsGradientPalette(this); } } } else { @@ -79,10 +79,9 @@ public class ColorExtractionService extends IntentService { } if (FeatureFlags.QSB_IN_HOTSEAT || FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) { - Palette wallpaperPalette = getWallpaperPalette(); - extractedColors.updateWallpaperThemePalette(wallpaperPalette); + extractedColors.updateWallpaperThemePalette(getWallpaperPalette()); if (FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) { - extractedColors.updateAllAppsGradientPalette(wallpaperPalette); + extractedColors.updateAllAppsGradientPalette(this); } } } diff --git a/src/com/android/launcher3/dynamicui/ExtractedColors.java b/src/com/android/launcher3/dynamicui/ExtractedColors.java index 3c4aba130..e72ab3d93 100644 --- a/src/com/android/launcher3/dynamicui/ExtractedColors.java +++ b/src/com/android/launcher3/dynamicui/ExtractedColors.java @@ -16,6 +16,7 @@ package com.android.launcher3.dynamicui; +import android.app.WallpaperManager; import android.content.Context; import android.graphics.Color; import android.support.annotation.Nullable; @@ -25,6 +26,7 @@ import android.util.Log; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.dynamicui.colorextraction.ColorExtractor; import java.util.Arrays; @@ -161,14 +163,17 @@ public class ExtractedColors { ? defaultColor : wallpaperPalette.getVibrantColor(defaultColor)); } - public void updateAllAppsGradientPalette(@Nullable Palette wallpaperPalette) { - // TODO b/37089857 will be modified to take the system extracted colors into account - int idx; - idx = ALLAPPS_GRADIENT_MAIN_INDEX; - setColorAtIndex(idx, wallpaperPalette == null - ? DEFAULT_VALUES[idx] : wallpaperPalette.getDarkVibrantColor(DEFAULT_VALUES[idx])); - idx = ALLAPPS_GRADIENT_SECONDARY_INDEX; - setColorAtIndex(idx, wallpaperPalette == null - ? DEFAULT_VALUES[idx] : wallpaperPalette.getVibrantColor(DEFAULT_VALUES[idx])); + public void updateAllAppsGradientPalette(Context context) { + // TODO use isAtLeastO when available + try { + WallpaperManager.class.getDeclaredMethod("getWallpaperColors", int.class); + ColorExtractor extractor = new ColorExtractor(context); + ColorExtractor.GradientColors colors = extractor.getColors(WallpaperManager.FLAG_SYSTEM); + setColorAtIndex(ALLAPPS_GRADIENT_MAIN_INDEX, colors.getMainColor()); + setColorAtIndex(ALLAPPS_GRADIENT_SECONDARY_INDEX, colors.getSecondaryColor()); + } catch (NoSuchMethodException e) { + setColorAtIndex(ALLAPPS_GRADIENT_MAIN_INDEX, Color.WHITE); + setColorAtIndex(ALLAPPS_GRADIENT_SECONDARY_INDEX, Color.WHITE); + } } } diff --git a/src/com/android/launcher3/dynamicui/colorextraction/ColorExtractor.java b/src/com/android/launcher3/dynamicui/colorextraction/ColorExtractor.java new file mode 100644 index 000000000..153b52914 --- /dev/null +++ b/src/com/android/launcher3/dynamicui/colorextraction/ColorExtractor.java @@ -0,0 +1,136 @@ +package com.android.launcher3.dynamicui.colorextraction; + +import android.app.WallpaperManager; +import android.content.Context; +import android.graphics.Color; +import android.os.Parcelable; +import android.util.Log; + +import com.android.launcher3.dynamicui.colorextraction.types.ExtractionType; +import com.android.launcher3.dynamicui.colorextraction.types.Tonal; + +import java.lang.reflect.Method; + + +/** + * Class to process wallpaper colors and generate a tonal palette based on them. + * + * TODO remove this class if available by platform + */ +public class ColorExtractor { + private static final String TAG = "ColorExtractor"; + private static final int FALLBACK_COLOR = Color.WHITE; + + private int mMainFallbackColor = FALLBACK_COLOR; + private int mSecondaryFallbackColor = FALLBACK_COLOR; + private final GradientColors mSystemColors; + private final GradientColors mLockColors; + private final Context mContext; + private final ExtractionType mExtractionType; + + public ColorExtractor(Context context) { + mContext = context; + mSystemColors = new GradientColors(); + mLockColors = new GradientColors(); + mExtractionType = new Tonal(); + WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class); + + if (wallpaperManager == null) { + Log.w(TAG, "Can't listen to color changes!"); + } else { + Parcelable wallpaperColorsObj; + try { + Method method = WallpaperManager.class + .getDeclaredMethod("getWallpaperColors", int.class); + + wallpaperColorsObj = (Parcelable) method.invoke(wallpaperManager, + WallpaperManager.FLAG_SYSTEM); + extractInto(new WallpaperColorsCompat(wallpaperColorsObj), mSystemColors); + wallpaperColorsObj = (Parcelable) method.invoke(wallpaperManager, + WallpaperManager.FLAG_LOCK); + extractInto(new WallpaperColorsCompat(wallpaperColorsObj), mLockColors); + } catch (Exception e) { + Log.e(TAG, "reflection failed", e); + } + } + } + + 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"); + } + } + + private void extractInto(WallpaperColorsCompat inWallpaperColors, GradientColors outGradientColors) { + applyFallback(outGradientColors); + if (inWallpaperColors == null) { + return; + } + mExtractionType.extractInto(inWallpaperColors, outGradientColors); + } + + private void applyFallback(GradientColors outGradientColors) { + outGradientColors.setMainColor(mMainFallbackColor); + outGradientColors.setSecondaryColor(mSecondaryFallbackColor); + } + + public static class GradientColors { + private int mMainColor = FALLBACK_COLOR; + private int mSecondaryColor = FALLBACK_COLOR; + private boolean mSupportsDarkText; + + public void setMainColor(int mainColor) { + mMainColor = mainColor; + } + + public void setSecondaryColor(int secondaryColor) { + mSecondaryColor = secondaryColor; + } + + public void setSupportsDarkText(boolean supportsDarkText) { + mSupportsDarkText = supportsDarkText; + } + + public void set(GradientColors other) { + mMainColor = other.mMainColor; + mSecondaryColor = other.mSecondaryColor; + mSupportsDarkText = other.mSupportsDarkText; + } + + public int getMainColor() { + return mMainColor; + } + + public int getSecondaryColor() { + return mSecondaryColor; + } + + public boolean supportsDarkText() { + return mSupportsDarkText; + } + + @Override + public boolean equals(Object o) { + if (o == null || o.getClass() != getClass()) { + return false; + } + GradientColors other = (GradientColors) o; + return other.mMainColor == mMainColor && + other.mSecondaryColor == mSecondaryColor && + other.mSupportsDarkText == mSupportsDarkText; + } + + @Override + public int hashCode() { + int code = mMainColor; + code = 31 * code + mSecondaryColor; + code = 31 * code + (mSupportsDarkText ? 0 : 1); + return code; + } + } +} + diff --git a/src/com/android/launcher3/dynamicui/colorextraction/WallpaperColorsCompat.java b/src/com/android/launcher3/dynamicui/colorextraction/WallpaperColorsCompat.java new file mode 100644 index 000000000..f80a675cb --- /dev/null +++ b/src/com/android/launcher3/dynamicui/colorextraction/WallpaperColorsCompat.java @@ -0,0 +1,69 @@ +package com.android.launcher3.dynamicui.colorextraction; + +import android.graphics.Color; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pair; + +import java.util.List; + +/** + * A wrapper around platform implementation of WallpaperColors until the + * updated SDK is available. + * + * TODO remove this class if available by platform + */ +public class WallpaperColorsCompat implements Parcelable { + + private final Parcelable mObject; + + public WallpaperColorsCompat(Parcelable object) { + mObject = object; + } + + private Object invokeMethod(String methodName) { + try { + return mObject.getClass().getDeclaredMethod(methodName).invoke(mObject); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeParcelable(mObject, i); + } + + public static final Parcelable.Creator<WallpaperColorsCompat> CREATOR = + new Parcelable.Creator<WallpaperColorsCompat>() { + public WallpaperColorsCompat createFromParcel(Parcel source) { + Parcelable object = source.readParcelable(null); + return new WallpaperColorsCompat(object); + } + + public WallpaperColorsCompat[] newArray(int size) { + return new WallpaperColorsCompat[size]; + } + }; + + public List<Pair<Color, Integer>> getColors() { + try { + return (List<Pair<Color, Integer>>) invokeMethod("getColors"); + } catch (Exception e) { + return null; + } + } + + public boolean supportsDarkText() { + try { + return (Boolean) invokeMethod("supportsDarkText"); + } catch (Exception e) { + return false; + } + } +} diff --git a/src/com/android/launcher3/dynamicui/colorextraction/types/ExtractionType.java b/src/com/android/launcher3/dynamicui/colorextraction/types/ExtractionType.java new file mode 100644 index 000000000..166c7c6f4 --- /dev/null +++ b/src/com/android/launcher3/dynamicui/colorextraction/types/ExtractionType.java @@ -0,0 +1,23 @@ +package com.android.launcher3.dynamicui.colorextraction.types; + +import com.android.launcher3.dynamicui.colorextraction.ColorExtractor; +import com.android.launcher3.dynamicui.colorextraction.WallpaperColorsCompat; + + +/** + * Interface to allow various color extraction implementations. + * + * TODO remove this class if available by platform + */ +public interface ExtractionType { + + /** + * Executes color extraction by reading WallpaperColors and setting + * main and secondary colors on GradientColors. + * + * @param inWallpaperColors where to read from + * @param outGradientColors object that should receive the colors + */ + void extractInto(WallpaperColorsCompat inWallpaperColors, + ColorExtractor.GradientColors outGradientColors); +} diff --git a/src/com/android/launcher3/dynamicui/colorextraction/types/Tonal.java b/src/com/android/launcher3/dynamicui/colorextraction/types/Tonal.java new file mode 100644 index 000000000..1e165a382 --- /dev/null +++ b/src/com/android/launcher3/dynamicui/colorextraction/types/Tonal.java @@ -0,0 +1,299 @@ +package com.android.launcher3.dynamicui.colorextraction.types; + +import android.graphics.Color; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.graphics.ColorUtils; +import android.util.Log; +import android.util.Pair; + +import com.android.launcher3.dynamicui.colorextraction.ColorExtractor; +import com.android.launcher3.dynamicui.colorextraction.WallpaperColorsCompat; + +import java.util.Comparator; + + +/** + * Implementation of tonal color extraction + * + * TODO remove this class if available by platform + */ +public class Tonal implements ExtractionType { + private static final String TAG = "Tonal"; + + // Used for tonal palette fitting + private static final float FIT_WEIGHT_H = 1.0f; + private static final float FIT_WEIGHT_S = 1.0f; + private static final float FIT_WEIGHT_L = 10.0f; + + private static final float MIN_COLOR_OCCURRENCE = 0.1f; + private static final float MIN_LUMINOSITY = 0.5f; + + public void extractInto(WallpaperColorsCompat wallpaperColors, + ColorExtractor.GradientColors gradientColors) { + if (wallpaperColors.getColors().size() == 0) { + return; + } + // Tonal is not really a sort, it takes a color from the extracted + // palette and finds a best fit amongst a collection of pre-defined + // palettes. The best fit is tweaked to be closer to the source color + // and replaces the original palette + + // First find the most representative color in the image + populationSort(wallpaperColors); + // Calculate total + int total = 0; + for (Pair<Color, Integer> weightedColor : wallpaperColors.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()) { + float colorOccurrence = weightedColor.second / (float) total; + if (colorOccurrence < MIN_COLOR_OCCURRENCE) { + break; + } + + int colorValue = weightedColor.first.toArgb(); + ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue), + Color.blue(colorValue), hsl); + if (hsl[2] > MIN_LUMINOSITY) { + bestColor = weightedColor; + } + } + + // Fallback to first color + if (bestColor == null) { + bestColor = wallpaperColors.getColors().get(0); + } + + int colorValue = bestColor.first.toArgb(); + ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue), Color.blue(colorValue), + hsl); + hsl[0] /= 360.0f; // normalize + + // TODO, we're finding a tonal palette for a hue, not all components + TonalPalette palette = findTonalPalette(hsl[0]); + + // Fall back to population sort if we couldn't find a tonal palette + if (palette == null) { + Log.w(TAG, "Could not find a tonal palette!"); + return; + } + + int fitIndex = bestFit(palette, hsl[0], hsl[1], hsl[2]); + if (fitIndex == -1) { + Log.w(TAG, "Could not find best fit!"); + return; + } + 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[0]) * 360.0f; + hsl[1] = s[0]; + hsl[2] = l[0]; + gradientColors.setMainColor(ColorUtils.HSLToColor(hsl)); + + hsl[0] = fract(h[1]) * 360.0f; + hsl[1] = s[1]; + hsl[2] = l[1]; + gradientColors.setSecondaryColor(ColorUtils.HSLToColor(hsl)); + } + + private static void populationSort(@NonNull WallpaperColorsCompat wallpaperColors) { + wallpaperColors.getColors().sort(new Comparator<Pair<Color, Integer>>() { + @Override + public int compare(Pair<Color, Integer> a, Pair<Color, Integer> b) { + return b.second - a.second; + } + }); + } + + /** + * Offsets all colors by a delta, clamping values that go beyond what's + * supported on the color space. + * @param data what you want to fit + * @param v how big should be the offset + * @param index which index to calculate the delta against + * @param min minimum accepted value (clamp) + * @param max maximum accepted value (clamp) + * @return + */ + private static float[] fit(float[] data, float v, int index, float min, float max) { + float[] fitData = new float[data.length]; + float delta = v - data[index]; + + for (int i = 0; i < data.length; i++) { + fitData[i] = constrain(data[i] + delta, min, max); + } + + return fitData; + } + + // TODO no MathUtils + private static float constrain(float x, float min, float max) { + x = Math.min(x, max); + x = Math.max(x, min); + return x; + } + + /*function adjustSatLumForFit(val, points, fitIndex) { + var fitValue = lerpBetweenPoints(points, fitIndex); + var diff = val - fitValue; + + var newPoints = []; + for (var ii=0; ii<points.length; ii++) { + var point = [points[ii][0], points[ii][1]]; + point[1] += diff; + if (point[1] > 1) point[1] = 1; + if (point[1] < 0) point[1] = 0; + newPoints[ii] = point; + } + return newPoints; + }*/ + + /** + * Finds the closest color in a palette, given another HSL color + * + * @param palette where to search + * @param h hue + * @param s saturation + * @param l lightness + * @return closest index or -1 if palette is empty. + */ + private static int bestFit(@NonNull TonalPalette palette, float h, float s, float l) { + int minErrorIndex = -1; + float minError = Float.POSITIVE_INFINITY; + + for (int i = 0; i < palette.h.length; i++) { + float error = + FIT_WEIGHT_H * Math.abs(h - palette.h[i]) + + FIT_WEIGHT_S * Math.abs(s - palette.s[i]) + + FIT_WEIGHT_L * Math.abs(l - palette.l[i]); + if (error < minError) { + minError = error; + minErrorIndex = i; + } + } + + return minErrorIndex; + } + + @Nullable + private static TonalPalette findTonalPalette(float h) { + TonalPalette best = null; + float error = Float.POSITIVE_INFINITY; + + for (TonalPalette candidate : TONAL_PALETTES) { + if (h >= candidate.minHue && h <= candidate.maxHue) { + best = candidate; + break; + } + + if (candidate.maxHue > 1.0f && h >= 0.0f && h <= fract(candidate.maxHue)) { + best = candidate; + break; + } + + if (candidate.minHue < 0.0f && h >= fract(candidate.minHue) && h <= 1.0f) { + best = candidate; + break; + } + + if (h <= candidate.minHue && candidate.minHue - h < error) { + best = candidate; + error = candidate.minHue - h; + } else if (h >= candidate.maxHue && h - candidate.maxHue < error) { + best = candidate; + error = h - candidate.maxHue; + } else if (candidate.maxHue > 1.0f && h >= fract(candidate.maxHue) + && h - fract(candidate.maxHue) < error) { + best = candidate; + error = h - fract(candidate.maxHue); + } else if (candidate.minHue < 0.0f && h <= fract(candidate.minHue) + && fract(candidate.minHue) - h < error) { + best = candidate; + error = fract(candidate.minHue) - h; + } + } + + return best; + } + + private static float fract(float v) { + return v - (float) Math.floor(v); + } + + static class TonalPalette { + final float[] h; + final float[] s; + final float[] l; + final float minHue; + final float maxHue; + + TonalPalette(float[] h, float[] s, float[] l) { + this.h = h; + this.s = s; + this.l = l; + + float minHue = Float.POSITIVE_INFINITY; + float maxHue = Float.NEGATIVE_INFINITY; + + for (float v : h) { + minHue = Math.min(v, minHue); + maxHue = Math.max(v, maxHue); + } + + this.minHue = minHue; + this.maxHue = maxHue; + } + } + + // Data definition of Material Design tonal palettes + // When the sort type is set to TONAL, these palettes are used to find + // a best fist. Each palette is defined as 10 HSL colors + private static final TonalPalette[] TONAL_PALETTES = { + // Orange + new TonalPalette( + new float[] { 0.028f, 0.042f, 0.053f, 0.061f, 0.078f, 0.1f, 0.111f, 0.111f, 0.111f, 0.111f }, + new float[] { 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f }, + new float[] { 0.5f, 0.53f, 0.54f, 0.55f, 0.535f, 0.52f, 0.5f, 0.63f, 0.75f, 0.85f } + ), + // Yellow + new TonalPalette( + new float[] { 0.111f, 0.111f, 0.125f, 0.133f, 0.139f, 0.147f, 0.156f, 0.156f, 0.156f, 0.156f }, + new float[] { 1f, 0.942f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f }, + new float[] { 0.43f, 0.484f, 0.535f, 0.555f, 0.57f, 0.575f, 0.595f, 0.715f, 0.78f, 0.885f } + ), + // Green + new TonalPalette( + new float[] { 0.325f, 0.336f, 0.353f, 0.353f, 0.356f, 0.356f, 0.356f, 0.356f, 0.356f, 0.356f }, + new float[] { 1f, 1f, 0.852f, 0.754f, 0.639f, 0.667f, 0.379f, 0.542f, 1f, 1f }, + new float[] { 0.06f, 0.1f, 0.151f, 0.194f, 0.25f, 0.312f, 0.486f, 0.651f, 0.825f, 0.885f } + ), + // Blue + new TonalPalette( + new float[] { 0.631f, 0.603f, 0.592f, 0.586f, 0.572f, 0.544f, 0.519f, 0.519f, 0.519f, 0.519f }, + new float[] { 0.852f, 1f, 0.887f, 0.852f, 0.871f, 0.907f, 0.949f, 0.934f, 0.903f, 0.815f }, + new float[] { 0.34f, 0.38f, 0.482f, 0.497f, 0.536f, 0.571f, 0.608f, 0.696f, 0.794f, 0.892f } + ), + // Purple + new TonalPalette( + new float[] { 0.839f, 0.831f, 0.825f, 0.819f, 0.803f, 0.803f, 0.772f, 0.772f, 0.772f, 0.772f }, + new float[] { 1f, 1f, 1f, 1f, 1f, 1f, 0.769f, 0.701f, 0.612f, 0.403f }, + new float[] { 0.125f, 0.15f, 0.2f, 0.245f, 0.31f, 0.36f, 0.567f, 0.666f, 0.743f, 0.833f } + ), + // Red + new TonalPalette( + new float[] { 0.964f, 0.975f, 0.975f, 0.975f, 0.972f, 0.992f, 1.003f, 1.011f, 1.011f, 1.011f }, + new float[] { 0.869f, 0.802f, 0.739f, 0.903f, 1f, 1f, 1f, 1f, 1f, 1f }, + new float[] { 0.241f, 0.316f, 0.46f, 0.586f, 0.655f, 0.7f, 0.75f, 0.8f, 0.84f, 0.88f } + ) + }; +} + |