• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.monet;
18 
19 
20 import android.annotation.ColorInt;
21 import android.app.WallpaperColors;
22 import android.graphics.Color;
23 
24 import com.android.internal.graphics.ColorUtils;
25 
26 import com.google.ux.material.libmonet.dynamiccolor.DynamicScheme;
27 import com.google.ux.material.libmonet.hct.Hct;
28 import com.google.ux.material.libmonet.scheme.SchemeContent;
29 import com.google.ux.material.libmonet.scheme.SchemeExpressive;
30 import com.google.ux.material.libmonet.scheme.SchemeFruitSalad;
31 import com.google.ux.material.libmonet.scheme.SchemeMonochrome;
32 import com.google.ux.material.libmonet.scheme.SchemeNeutral;
33 import com.google.ux.material.libmonet.scheme.SchemeRainbow;
34 import com.google.ux.material.libmonet.scheme.SchemeTonalSpot;
35 import com.google.ux.material.libmonet.scheme.SchemeVibrant;
36 
37 import java.util.AbstractMap;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.stream.Collectors;
43 
44 /**
45  * @deprecated Please use com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors instead
46  */
47 @Deprecated
48 public class ColorScheme {
49     public static final int GOOGLE_BLUE = 0xFF1b6ef3;
50     private static final float ACCENT1_CHROMA = 48.0f;
51     private static final int MIN_CHROMA = 5;
52 
53     @ColorInt
54     private final int mSeed;
55     private final boolean mIsDark;
56     @Style.Type
57     private final  int mStyle;
58     private final DynamicScheme mMaterialScheme;
59     private final TonalPalette mAccent1;
60     private final TonalPalette mAccent2;
61     private final TonalPalette mAccent3;
62     private final TonalPalette mNeutral1;
63     private final TonalPalette mNeutral2;
64     private final TonalPalette mError;
65     private final Hct mProposedSeedHct;
66 
67 
ColorScheme(@olorInt int seed, boolean isDark, @Style.Type int style, double contrastLevel)68     public ColorScheme(@ColorInt int seed, boolean isDark, @Style.Type int style,
69             double contrastLevel) {
70         this.mSeed = seed;
71         this.mIsDark = isDark;
72         this.mStyle = style;
73 
74         mProposedSeedHct = Hct.fromInt(seed);
75         Hct seedHct = Hct.fromInt(
76                 seed == Color.TRANSPARENT
77                         ? GOOGLE_BLUE
78                         : (style != Style.CONTENT
79                                 && mProposedSeedHct.getChroma() < 5
80                                 ? GOOGLE_BLUE
81                                 : seed));
82 
83         mMaterialScheme = switch (style) {
84             case Style.SPRITZ -> new SchemeNeutral(seedHct, isDark, contrastLevel);
85             case Style.TONAL_SPOT -> new SchemeTonalSpot(seedHct, isDark, contrastLevel);
86             case Style.VIBRANT -> new SchemeVibrant(seedHct, isDark, contrastLevel);
87             case Style.EXPRESSIVE -> new SchemeExpressive(seedHct, isDark, contrastLevel);
88             case Style.RAINBOW -> new SchemeRainbow(seedHct, isDark, contrastLevel);
89             case Style.FRUIT_SALAD -> new SchemeFruitSalad(seedHct, isDark, contrastLevel);
90             case Style.CONTENT -> new SchemeContent(seedHct, isDark, contrastLevel);
91             case Style.MONOCHROMATIC -> new SchemeMonochrome(seedHct, isDark, contrastLevel);
92             // SystemUI Schemes
93             case Style.CLOCK -> new SchemeClock(seedHct, isDark, contrastLevel);
94             case Style.CLOCK_VIBRANT -> new SchemeClockVibrant(seedHct, isDark, contrastLevel);
95             default -> throw new IllegalArgumentException("Unknown style: " + style);
96         };
97 
98         mAccent1 = new TonalPalette(mMaterialScheme.primaryPalette);
99         mAccent2 = new TonalPalette(mMaterialScheme.secondaryPalette);
100         mAccent3 = new TonalPalette(mMaterialScheme.tertiaryPalette);
101         mNeutral1 = new TonalPalette(mMaterialScheme.neutralPalette);
102         mNeutral2 = new TonalPalette(mMaterialScheme.neutralVariantPalette);
103         mError = new TonalPalette(mMaterialScheme.errorPalette);
104     }
105 
ColorScheme(@olorInt int seed, boolean darkTheme)106     public ColorScheme(@ColorInt int seed, boolean darkTheme) {
107         this(seed, darkTheme, Style.TONAL_SPOT);
108     }
109 
ColorScheme(@olorInt int seed, boolean darkTheme, @Style.Type int style)110     public ColorScheme(@ColorInt int seed, boolean darkTheme, @Style.Type int style) {
111         this(seed, darkTheme, style, 0.0);
112     }
113 
ColorScheme(WallpaperColors wallpaperColors, boolean darkTheme, @Style.Type int style)114     public ColorScheme(WallpaperColors wallpaperColors, boolean darkTheme, @Style.Type int style) {
115         this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style);
116     }
117 
ColorScheme(WallpaperColors wallpaperColors, boolean darkTheme)118     public ColorScheme(WallpaperColors wallpaperColors, boolean darkTheme) {
119         this(wallpaperColors, darkTheme, Style.TONAL_SPOT);
120     }
121 
getBackgroundColor()122     public int getBackgroundColor() {
123         return ColorUtils.setAlphaComponent(mIsDark
124                 ? mNeutral1.getS700()
125                 : mNeutral1.getS10(), 0xFF);
126     }
127 
getAccentColor()128     public int getAccentColor() {
129         return ColorUtils.setAlphaComponent(mIsDark
130                 ? mAccent1.getS100()
131                 : mAccent1.getS500(), 0xFF);
132     }
133 
getSeedTone()134     public double getSeedTone() {
135         return 1000d - mProposedSeedHct.getTone() * 10d;
136     }
137 
getSeed()138     public int getSeed() {
139         return mSeed;
140     }
141 
142     @Style.Type
getStyle()143     public int getStyle() {
144         return mStyle;
145     }
146 
getMaterialScheme()147     public DynamicScheme getMaterialScheme() {
148         return mMaterialScheme;
149     }
150 
getAccent1()151     public TonalPalette getAccent1() {
152         return mAccent1;
153     }
154 
getAccent2()155     public TonalPalette getAccent2() {
156         return mAccent2;
157     }
158 
getAccent3()159     public TonalPalette getAccent3() {
160         return mAccent3;
161     }
162 
getNeutral1()163     public TonalPalette getNeutral1() {
164         return mNeutral1;
165     }
166 
getNeutral2()167     public TonalPalette getNeutral2() {
168         return mNeutral2;
169     }
170 
getError()171     public TonalPalette getError() {
172         return mError;
173     }
174 
175     @Override
toString()176     public String toString() {
177         return "ColorScheme {\n"
178                 + "  seed color: " + stringForColor(mSeed) + "\n"
179                 + "  style: " + mStyle + "\n"
180                 + "  palettes: \n"
181                 + "  " + humanReadable("PRIMARY", mAccent1.allShades) + "\n"
182                 + "  " + humanReadable("SECONDARY", mAccent2.allShades) + "\n"
183                 + "  " + humanReadable("TERTIARY", mAccent3.allShades) + "\n"
184                 + "  " + humanReadable("NEUTRAL", mNeutral1.allShades) + "\n"
185                 + "  " + humanReadable("NEUTRAL VARIANT", mNeutral2.allShades) + "\n"
186                 + "}";
187     }
188 
189     /**
190      * Identifies a color to create a color scheme from.
191      *
192      * @param wallpaperColors Colors extracted from an image via quantization.
193      * @param filter          If false, allow colors that have low chroma, creating grayscale
194      *                        themes.
195      * @return ARGB int representing the color
196      */
197     @ColorInt
getSeedColor(WallpaperColors wallpaperColors, boolean filter)198     public static int getSeedColor(WallpaperColors wallpaperColors, boolean filter) {
199         return getSeedColors(wallpaperColors, filter).get(0);
200     }
201 
202     /**
203      * Identifies a color to create a color scheme from. Defaults Filter to TRUE
204      *
205      * @param wallpaperColors Colors extracted from an image via quantization.
206      * @return ARGB int representing the color
207      */
getSeedColor(WallpaperColors wallpaperColors)208     public static int getSeedColor(WallpaperColors wallpaperColors) {
209         return getSeedColor(wallpaperColors, true);
210     }
211 
212     /**
213      * Filters and ranks colors from WallpaperColors.
214      *
215      * @param wallpaperColors Colors extracted from an image via quantization.
216      * @param filter          If false, allow colors that have low chroma, creating grayscale
217      *                        themes.
218      * @return List of ARGB ints, ordered from highest scoring to lowest.
219      */
getSeedColors(WallpaperColors wallpaperColors, boolean filter)220     public static List<Integer> getSeedColors(WallpaperColors wallpaperColors, boolean filter) {
221         double totalPopulation = wallpaperColors.getAllColors().values().stream().mapToInt(
222                 Integer::intValue).sum();
223         boolean totalPopulationMeaningless = (totalPopulation == 0.0);
224 
225         if (totalPopulationMeaningless) {
226             // WallpaperColors with a population of 0 indicate the colors didn't come from
227             // quantization. Instead of scoring, trust the ordering of the provided primary
228             // secondary/tertiary colors.
229             //
230             // In this case, the colors are usually from a Live Wallpaper.
231             List<Integer> distinctColors = wallpaperColors.getMainColors().stream()
232                     .map(Color::toArgb)
233                     .distinct()
234                     .filter(color -> !filter || Hct.fromInt(color).getChroma() >= MIN_CHROMA)
235                     .collect(Collectors.toList());
236             if (distinctColors.isEmpty()) {
237                 return List.of(GOOGLE_BLUE);
238             }
239             return distinctColors;
240         }
241 
242         Map<Integer, Double> intToProportion = wallpaperColors.getAllColors().entrySet().stream()
243                 .collect(Collectors.toMap(Map.Entry::getKey,
244                         entry -> entry.getValue().doubleValue() / totalPopulation));
245         Map<Integer, Hct> intToHct = wallpaperColors.getAllColors().entrySet().stream()
246                 .collect(Collectors.toMap(Map.Entry::getKey, entry -> Hct.fromInt(entry.getKey())));
247 
248         // Get an array with 360 slots. A slot contains the percentage of colors with that hue.
249         List<Double> hueProportions = huePopulations(intToHct, intToProportion, filter);
250         // Map each color to the percentage of the image with its hue.
251         Map<Integer, Double> intToHueProportion = wallpaperColors.getAllColors().entrySet().stream()
252                 .collect(Collectors.toMap(Map.Entry::getKey, entry -> {
253                     Hct hct = intToHct.get(entry.getKey());
254                     int hue = (int) Math.round(hct.getHue());
255                     double proportion = 0.0;
256                     for (int i = hue - 15; i <= hue + 15; i++) {
257                         proportion += hueProportions.get(wrapDegrees(i));
258                     }
259                     return proportion;
260                 }));
261         // Remove any inappropriate seed colors. For example, low chroma colors look grayscale
262         // raising their chroma will turn them to a much louder color that may not have been
263         // in the image.
264         Map<Integer, Hct> filteredIntToHct = filter
265                 ? intToHct
266                 .entrySet()
267                 .stream()
268                 .filter(entry -> {
269                     Hct hct = entry.getValue();
270                     double proportion = intToHueProportion.get(entry.getKey());
271                     return hct.getChroma() >= MIN_CHROMA && proportion > 0.01;
272                 })
273                 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
274                 : intToHct;
275         // Sort the colors by score, from high to low.
276         List<Map.Entry<Integer, Double>> intToScore = filteredIntToHct.entrySet().stream()
277                 .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(),
278                         score(entry.getValue(), intToHueProportion.get(entry.getKey()))))
279                 .sorted(Map.Entry.<Integer, Double>comparingByValue().reversed())
280                 .collect(Collectors.toList());
281 
282         // Go through the colors, from high score to low score.
283         // If the color is distinct in hue from colors picked so far, pick the color.
284         // Iteratively decrease the amount of hue distinctness required, thus ensuring we
285         // maximize difference between colors.
286         int minimumHueDistance = 15;
287         List<Integer> seeds = new ArrayList<>();
288         for (int i = 90; i >= minimumHueDistance; i--) {
289             seeds.clear();
290             for (Map.Entry<Integer, Double> entry : intToScore) {
291                 int currentColor = entry.getKey();
292                 int finalI = i;
293                 boolean existingSeedNearby = seeds.stream().anyMatch(seed -> {
294                     double hueA = intToHct.get(currentColor).getHue();
295                     double hueB = intToHct.get(seed).getHue();
296                     return hueDiff(hueA, hueB) < finalI;
297                 });
298                 if (existingSeedNearby) {
299                     continue;
300                 }
301                 seeds.add(currentColor);
302                 if (seeds.size() >= 4) {
303                     break;
304                 }
305             }
306             if (!seeds.isEmpty()) {
307                 break;
308             }
309         }
310 
311         if (seeds.isEmpty()) {
312             // Use gBlue 500 if there are 0 colors
313             seeds.add(GOOGLE_BLUE);
314         }
315 
316         return seeds;
317     }
318 
319     /**
320      * Filters and ranks colors from WallpaperColors. Defaults Filter to TRUE
321      *
322      * @param newWallpaperColors Colors extracted from an image via quantization.
323      *                           themes.
324      * @return List of ARGB ints, ordered from highest scoring to lowest.
325      */
getSeedColors(WallpaperColors newWallpaperColors)326     public static List<Integer> getSeedColors(WallpaperColors newWallpaperColors) {
327         return getSeedColors(newWallpaperColors, true);
328     }
329 
wrapDegrees(int degrees)330     private static int wrapDegrees(int degrees) {
331         if (degrees < 0) {
332             return (degrees % 360) + 360;
333         } else if (degrees >= 360) {
334             return degrees % 360;
335         } else {
336             return degrees;
337         }
338     }
339 
hueDiff(double a, double b)340     private static double hueDiff(double a, double b) {
341         double diff = Math.abs(a - b);
342         if (diff > 180f) {
343             // 0 and 360 are the same hue. If hue difference is greater than 180, subtract from 360
344             // to account for the circularity.
345             diff = 360f - diff;
346         }
347         return diff;
348     }
349 
stringForColor(int color)350     private static String stringForColor(int color) {
351         int width = 4;
352         Hct hct = Hct.fromInt(color);
353         String h = "H" + String.format("%" + width + "s", Math.round(hct.getHue()));
354         String c = "C" + String.format("%" + width + "s", Math.round(hct.getChroma()));
355         String t = "T" + String.format("%" + width + "s", Math.round(hct.getTone()));
356         String hex = Integer.toHexString(color & 0xffffff).toUpperCase();
357         return h + c + t + " = #" + hex;
358     }
359 
humanReadable(String paletteName, List<Integer> colors)360     private static String humanReadable(String paletteName, List<Integer> colors) {
361         return paletteName + "\n"
362                 + colors
363                 .stream()
364                 .map(ColorScheme::stringForColor)
365                 .collect(Collectors.joining("\n"));
366     }
367 
score(Hct hct, double proportion)368     private static double score(Hct hct, double proportion) {
369         double proportionScore = 0.7 * 100.0 * proportion;
370         double chromaScore = hct.getChroma() < ACCENT1_CHROMA
371                 ? 0.1 * (hct.getChroma() - ACCENT1_CHROMA)
372                 : 0.3 * (hct.getChroma() - ACCENT1_CHROMA);
373         return chromaScore + proportionScore;
374     }
375 
376     private static List<Double> huePopulations(Map<Integer, Hct> hctByColor,
377             Map<Integer, Double> populationByColor, boolean filter) {
378         List<Double> huePopulation = new ArrayList<>(Collections.nCopies(360, 0.0));
379 
380         for (Map.Entry<Integer, Double> entry : populationByColor.entrySet()) {
381             double population = entry.getValue();
382             Hct hct = hctByColor.get(entry.getKey());
383             int hue = (int) Math.round(hct.getHue()) % 360;
384             if (filter && hct.getChroma() <= MIN_CHROMA) {
385                 continue;
386             }
387             huePopulation.set(hue, huePopulation.get(hue) + population);
388         }
389 
390         return huePopulation;
391     }
392 }
393