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