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