🎨 Jetpack Compose material theming library, which falls back onto a custom dynamic colors implementation based on wallpapers to support older API levels. 19 | Yes! Dynamic colors are now available even on old android devices, enjoy!
20 | 21 | # Examples (See [ImageResizer](https://github.com/t8rin/imageResizer)) 22 | 23 |
24 |
25 |
26 |
27 |
CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar, 28 | * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and should be used when 29 | * measuring distances between colors. 30 | * 31 | *
In traditional color spaces, a color can be identified solely by the observer's measurement of 32 | * the color. Color appearance models such as CAM16 also use information about the environment where 33 | * the color was observed, known as the viewing conditions. 34 | * 35 | *
For example, white under the traditional assumption of a midday sun white point is accurately 36 | * measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100) 37 | */ 38 | public final class Cam16 { 39 | // Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16. 40 | static final double[][] XYZ_TO_CAM16RGB = { 41 | {0.401288, 0.650173, -0.051461}, 42 | {-0.250268, 1.204414, 0.045854}, 43 | {-0.002079, 0.048952, 0.953127} 44 | }; 45 | 46 | // Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates. 47 | static final double[][] CAM16RGB_TO_XYZ = { 48 | {1.8620678, -1.0112547, 0.14918678}, 49 | {0.38752654, 0.62144744, -0.00897398}, 50 | {-0.01584150, -0.03412294, 1.0499644} 51 | }; 52 | 53 | // CAM16 color dimensions, see getters for documentation. 54 | private final double hue; 55 | private final double chroma; 56 | private final double j; 57 | private final double q; 58 | private final double m; 59 | private final double s; 60 | 61 | // Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*. 62 | private final double jstar; 63 | private final double astar; 64 | private final double bstar; 65 | 66 | // Avoid allocations during conversion by pre-allocating an array. 67 | private final double[] tempArray = new double[]{0.0, 0.0, 0.0}; 68 | 69 | /** 70 | * All of the CAM16 dimensions can be calculated from 3 of the dimensions, in the following 71 | * combinations: - {j or q} and {c, m, or s} and hue - jstar, astar, bstar Prefer using a static 72 | * method that constructs from 3 of those dimensions. This constructor is intended for those 73 | * methods to use to return all possible dimensions. 74 | * 75 | * @param hue for example, red, orange, yellow, green, etc. 76 | * @param chroma informally, colorfulness / color intensity. like saturation in HSL, except 77 | * perceptually accurate. 78 | * @param j lightness 79 | * @param q brightness; ratio of lightness to white point's lightness 80 | * @param m colorfulness 81 | * @param s saturation; ratio of chroma to white point's chroma 82 | * @param jstar CAM16-UCS J coordinate 83 | * @param astar CAM16-UCS a coordinate 84 | * @param bstar CAM16-UCS b coordinate 85 | */ 86 | private Cam16( 87 | double hue, 88 | double chroma, 89 | double j, 90 | double q, 91 | double m, 92 | double s, 93 | double jstar, 94 | double astar, 95 | double bstar) { 96 | this.hue = hue; 97 | this.chroma = chroma; 98 | this.j = j; 99 | this.q = q; 100 | this.m = m; 101 | this.s = s; 102 | this.jstar = jstar; 103 | this.astar = astar; 104 | this.bstar = bstar; 105 | } 106 | 107 | /** 108 | * Create a CAM16 color from a color, assuming the color was viewed in default viewing conditions. 109 | * 110 | * @param argb ARGB representation of a color. 111 | */ 112 | public static Cam16 fromInt(int argb) { 113 | return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT); 114 | } 115 | 116 | /** 117 | * Create a CAM16 color from a color in defined viewing conditions. 118 | * 119 | * @param argb ARGB representation of a color. 120 | * @param viewingConditions Information about the environment where the color was observed. 121 | */ 122 | // The RGB => XYZ conversion matrix elements are derived scientific constants. While the values 123 | // may differ at runtime due to floating point imprecision, keeping the values the same, and 124 | // accurate, across implementations takes precedence. 125 | @SuppressWarnings("FloatingPointLiteralPrecision") 126 | static Cam16 fromIntInViewingConditions(int argb, ViewingConditions viewingConditions) { 127 | // Transform ARGB int to XYZ 128 | int red = (argb & 0x00ff0000) >> 16; 129 | int green = (argb & 0x0000ff00) >> 8; 130 | int blue = (argb & 0x000000ff); 131 | double redL = ColorUtils.linearized(red); 132 | double greenL = ColorUtils.linearized(green); 133 | double blueL = ColorUtils.linearized(blue); 134 | double x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL; 135 | double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL; 136 | double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL; 137 | 138 | return fromXyzInViewingConditions(x, y, z, viewingConditions); 139 | } 140 | 141 | static Cam16 fromXyzInViewingConditions( 142 | double x, double y, double z, ViewingConditions viewingConditions) { 143 | // Transform XYZ to 'cone'/'rgb' responses 144 | double[][] matrix = XYZ_TO_CAM16RGB; 145 | double rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]); 146 | double gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]); 147 | double bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]); 148 | 149 | // Discount illuminant 150 | double rD = viewingConditions.getRgbD()[0] * rT; 151 | double gD = viewingConditions.getRgbD()[1] * gT; 152 | double bD = viewingConditions.getRgbD()[2] * bT; 153 | 154 | // Chromatic adaptation 155 | double rAF = Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42); 156 | double gAF = Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42); 157 | double bAF = Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42); 158 | double rA = Math.signum(rD) * 400.0 * rAF / (rAF + 27.13); 159 | double gA = Math.signum(gD) * 400.0 * gAF / (gAF + 27.13); 160 | double bA = Math.signum(bD) * 400.0 * bAF / (bAF + 27.13); 161 | 162 | // redness-greenness 163 | double a = (11.0 * rA + -12.0 * gA + bA) / 11.0; 164 | // yellowness-blueness 165 | double b = (rA + gA - 2.0 * bA) / 9.0; 166 | 167 | // auxiliary components 168 | double u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0; 169 | double p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0; 170 | 171 | // hue 172 | double atan2 = Math.atan2(b, a); 173 | double atanDegrees = Math.toDegrees(atan2); 174 | double hue = 175 | atanDegrees < 0 176 | ? atanDegrees + 360.0 177 | : atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees; 178 | double hueRadians = Math.toRadians(hue); 179 | 180 | // achromatic response to color 181 | double ac = p2 * viewingConditions.getNbb(); 182 | 183 | // CAM16 lightness and brightness 184 | double j = 185 | 100.0 186 | * Math.pow( 187 | ac / viewingConditions.getAw(), 188 | viewingConditions.getC() * viewingConditions.getZ()); 189 | double q = 190 | 4.0 191 | / viewingConditions.getC() 192 | * Math.sqrt(j / 100.0) 193 | * (viewingConditions.getAw() + 4.0) 194 | * viewingConditions.getFlRoot(); 195 | 196 | // CAM16 chroma, colorfulness, and saturation. 197 | double huePrime = (hue < 20.14) ? hue + 360 : hue; 198 | double eHue = 0.25 * (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8); 199 | double p1 = 50000.0 / 13.0 * eHue * viewingConditions.getNc() * viewingConditions.getNcb(); 200 | double t = p1 * Math.hypot(a, b) / (u + 0.305); 201 | double alpha = 202 | Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) * Math.pow(t, 0.9); 203 | // CAM16 chroma, colorfulness, saturation 204 | double c = alpha * Math.sqrt(j / 100.0); 205 | double m = c * viewingConditions.getFlRoot(); 206 | double s = 207 | 50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0)); 208 | 209 | // CAM16-UCS components 210 | double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j); 211 | double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m); 212 | double astar = mstar * Math.cos(hueRadians); 213 | double bstar = mstar * Math.sin(hueRadians); 214 | 215 | return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar); 216 | } 217 | 218 | /** 219 | * @param j CAM16 lightness 220 | * @param c CAM16 chroma 221 | * @param h CAM16 hue 222 | */ 223 | static Cam16 fromJch(double j, double c, double h) { 224 | return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT); 225 | } 226 | 227 | /** 228 | * @param j CAM16 lightness 229 | * @param c CAM16 chroma 230 | * @param h CAM16 hue 231 | * @param viewingConditions Information about the environment where the color was observed. 232 | */ 233 | private static Cam16 fromJchInViewingConditions( 234 | double j, double c, double h, ViewingConditions viewingConditions) { 235 | double q = 236 | 4.0 237 | / viewingConditions.getC() 238 | * Math.sqrt(j / 100.0) 239 | * (viewingConditions.getAw() + 4.0) 240 | * viewingConditions.getFlRoot(); 241 | double m = c * viewingConditions.getFlRoot(); 242 | double alpha = c / Math.sqrt(j / 100.0); 243 | double s = 244 | 50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0)); 245 | 246 | double hueRadians = Math.toRadians(h); 247 | double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j); 248 | double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m); 249 | double astar = mstar * Math.cos(hueRadians); 250 | double bstar = mstar * Math.sin(hueRadians); 251 | return new Cam16(h, c, j, q, m, s, jstar, astar, bstar); 252 | } 253 | 254 | /** 255 | * Create a CAM16 color from CAM16-UCS coordinates. 256 | * 257 | * @param jstar CAM16-UCS lightness. 258 | * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y 259 | * axis. 260 | * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X 261 | * axis. 262 | */ 263 | public static Cam16 fromUcs(double jstar, double astar, double bstar) { 264 | 265 | return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT); 266 | } 267 | 268 | /** 269 | * Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions. 270 | * 271 | * @param jstar CAM16-UCS lightness. 272 | * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y 273 | * axis. 274 | * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X 275 | * axis. 276 | * @param viewingConditions Information about the environment where the color was observed. 277 | */ 278 | public static Cam16 fromUcsInViewingConditions( 279 | double jstar, double astar, double bstar, ViewingConditions viewingConditions) { 280 | 281 | double m = Math.hypot(astar, bstar); 282 | double m2 = Math.expm1(m * 0.0228) / 0.0228; 283 | double c = m2 / viewingConditions.getFlRoot(); 284 | double h = Math.atan2(bstar, astar) * (180.0 / Math.PI); 285 | if (h < 0.0) { 286 | h += 360.0; 287 | } 288 | double j = jstar / (1. - (jstar - 100.) * 0.007); 289 | return fromJchInViewingConditions(j, c, h, viewingConditions); 290 | } 291 | 292 | /** 293 | * CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar, 294 | * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure 295 | * distances between colors. 296 | */ 297 | double distance(Cam16 other) { 298 | double dJ = getJstar() - other.getJstar(); 299 | double dA = getAstar() - other.getAstar(); 300 | double dB = getBstar() - other.getBstar(); 301 | double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB); 302 | double dE = 1.41 * Math.pow(dEPrime, 0.63); 303 | return dE; 304 | } 305 | 306 | /** 307 | * Hue in CAM16 308 | */ 309 | public double getHue() { 310 | return hue; 311 | } 312 | 313 | /** 314 | * Chroma in CAM16 315 | */ 316 | public double getChroma() { 317 | return chroma; 318 | } 319 | 320 | /** 321 | * Lightness in CAM16 322 | */ 323 | public double getJ() { 324 | return j; 325 | } 326 | 327 | /** 328 | * Brightness in CAM16. 329 | * 330 | *
Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper is 331 | * much brighter viewed in sunlight than in indoor light, but it is the lightest object under any 332 | * lighting. 333 | */ 334 | public double getQ() { 335 | return q; 336 | } 337 | 338 | /** 339 | * Colorfulness in CAM16. 340 | * 341 | *
Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much 342 | * more colorful outside than inside, but it has the same chroma in both environments. 343 | */ 344 | public double getM() { 345 | return m; 346 | } 347 | 348 | /** 349 | * Saturation in CAM16. 350 | * 351 | *
Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
352 | * relative to the color's own brightness, where chroma is colorfulness relative to white.
353 | */
354 | public double getS() {
355 | return s;
356 | }
357 |
358 | /**
359 | * Lightness coordinate in CAM16-UCS
360 | */
361 | public double getJstar() {
362 | return jstar;
363 | }
364 |
365 | /**
366 | * a* coordinate in CAM16-UCS
367 | */
368 | public double getAstar() {
369 | return astar;
370 | }
371 |
372 | /**
373 | * b* coordinate in CAM16-UCS
374 | */
375 | public double getBstar() {
376 | return bstar;
377 | }
378 |
379 | /**
380 | * ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
381 | * which are near-identical to the default viewing conditions for sRGB.
382 | */
383 | public int toInt() {
384 | return viewed(ViewingConditions.DEFAULT);
385 | }
386 |
387 | /**
388 | * ARGB representation of the color, in defined viewing conditions.
389 | *
390 | * @param viewingConditions Information about the environment where the color will be viewed.
391 | * @return ARGB representation of color
392 | */
393 | int viewed(ViewingConditions viewingConditions) {
394 | double[] xyz = xyzInViewingConditions(viewingConditions, tempArray);
395 | return ColorUtils.argbFromXyz(xyz[0], xyz[1], xyz[2]);
396 | }
397 |
398 | double[] xyzInViewingConditions(ViewingConditions viewingConditions, double[] returnArray) {
399 | double alpha =
400 | (getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(getJ() / 100.0);
401 |
402 | double t =
403 | Math.pow(
404 | alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9);
405 | double hRad = Math.toRadians(getHue());
406 |
407 | double eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8);
408 | double ac =
409 | viewingConditions.getAw()
410 | * Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
411 | double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
412 | double p2 = (ac / viewingConditions.getNbb());
413 |
414 | double hSin = Math.sin(hRad);
415 | double hCos = Math.cos(hRad);
416 |
417 | double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin);
418 | double a = gamma * hCos;
419 | double b = gamma * hSin;
420 | double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
421 | double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
422 | double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
423 |
424 | double rCBase = max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
425 | double rC =
426 | Math.signum(rA) * (100.0 / viewingConditions.getFl()) * Math.pow(rCBase, 1.0 / 0.42);
427 | double gCBase = max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
428 | double gC =
429 | Math.signum(gA) * (100.0 / viewingConditions.getFl()) * Math.pow(gCBase, 1.0 / 0.42);
430 | double bCBase = max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
431 | double bC =
432 | Math.signum(bA) * (100.0 / viewingConditions.getFl()) * Math.pow(bCBase, 1.0 / 0.42);
433 | double rF = rC / viewingConditions.getRgbD()[0];
434 | double gF = gC / viewingConditions.getRgbD()[1];
435 | double bF = bC / viewingConditions.getRgbD()[2];
436 |
437 | double[][] matrix = CAM16RGB_TO_XYZ;
438 | double x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]);
439 | double y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]);
440 | double z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);
441 |
442 | if (returnArray != null) {
443 | returnArray[0] = x;
444 | returnArray[1] = y;
445 | returnArray[2] = z;
446 | return returnArray;
447 | } else {
448 | return new double[]{x, y, z};
449 | }
450 | }
451 | }
452 |
--------------------------------------------------------------------------------
/dynamic_theme/src/main/java/com/t8rin/dynamic/theme/scheme/Scheme.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Google LLC
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 | // This file is automatically generated. Do not modify it.
17 | package com.t8rin.dynamic.theme.scheme
18 |
19 | import com.t8rin.dynamic.theme.palettes.CorePalette
20 | import com.t8rin.dynamic.theme.palettes.CorePalette.Companion.contentOf
21 | import com.t8rin.dynamic.theme.palettes.CorePalette.Companion.of
22 |
23 | /**
24 | * Represents a Material color scheme, a mapping of color roles to colors.
25 | */
26 | class Scheme {
27 | var primary = 0
28 | var onPrimary = 0
29 | var primaryContainer = 0
30 | var onPrimaryContainer = 0
31 | var secondary = 0
32 | var onSecondary = 0
33 | var secondaryContainer = 0
34 | var onSecondaryContainer = 0
35 | var tertiary = 0
36 | var onTertiary = 0
37 | var tertiaryContainer = 0
38 | var onTertiaryContainer = 0
39 | var error = 0
40 | var onError = 0
41 | var errorContainer = 0
42 | var onErrorContainer = 0
43 | var background = 0
44 | var onBackground = 0
45 | var surface = 0
46 | var onSurface = 0
47 | var surfaceVariant = 0
48 | var onSurfaceVariant = 0
49 | var outline = 0
50 | var outlineVariant = 0
51 | var shadow = 0
52 | var scrim = 0
53 | var inverseSurface = 0
54 | var inverseOnSurface = 0
55 | var inversePrimary = 0
56 |
57 | constructor() {}
58 | constructor(
59 | primary: Int,
60 | onPrimary: Int,
61 | primaryContainer: Int,
62 | onPrimaryContainer: Int,
63 | secondary: Int,
64 | onSecondary: Int,
65 | secondaryContainer: Int,
66 | onSecondaryContainer: Int,
67 | tertiary: Int,
68 | onTertiary: Int,
69 | tertiaryContainer: Int,
70 | onTertiaryContainer: Int,
71 | error: Int,
72 | onError: Int,
73 | errorContainer: Int,
74 | onErrorContainer: Int,
75 | background: Int,
76 | onBackground: Int,
77 | surface: Int,
78 | onSurface: Int,
79 | surfaceVariant: Int,
80 | onSurfaceVariant: Int,
81 | outline: Int,
82 | outlineVariant: Int,
83 | shadow: Int,
84 | scrim: Int,
85 | inverseSurface: Int,
86 | inverseOnSurface: Int,
87 | inversePrimary: Int
88 | ) : super() {
89 | this.primary = primary
90 | this.onPrimary = onPrimary
91 | this.primaryContainer = primaryContainer
92 | this.onPrimaryContainer = onPrimaryContainer
93 | this.secondary = secondary
94 | this.onSecondary = onSecondary
95 | this.secondaryContainer = secondaryContainer
96 | this.onSecondaryContainer = onSecondaryContainer
97 | this.tertiary = tertiary
98 | this.onTertiary = onTertiary
99 | this.tertiaryContainer = tertiaryContainer
100 | this.onTertiaryContainer = onTertiaryContainer
101 | this.error = error
102 | this.onError = onError
103 | this.errorContainer = errorContainer
104 | this.onErrorContainer = onErrorContainer
105 | this.background = background
106 | this.onBackground = onBackground
107 | this.surface = surface
108 | this.onSurface = onSurface
109 | this.surfaceVariant = surfaceVariant
110 | this.onSurfaceVariant = onSurfaceVariant
111 | this.outline = outline
112 | this.outlineVariant = outlineVariant
113 | this.shadow = shadow
114 | this.scrim = scrim
115 | this.inverseSurface = inverseSurface
116 | this.inverseOnSurface = inverseOnSurface
117 | this.inversePrimary = inversePrimary
118 | }
119 |
120 | fun withPrimary(primary: Int): Scheme {
121 | this.primary = primary
122 | return this
123 | }
124 |
125 | fun withOnPrimary(onPrimary: Int): Scheme {
126 | this.onPrimary = onPrimary
127 | return this
128 | }
129 |
130 | fun withPrimaryContainer(primaryContainer: Int): Scheme {
131 | this.primaryContainer = primaryContainer
132 | return this
133 | }
134 |
135 | fun withOnPrimaryContainer(onPrimaryContainer: Int): Scheme {
136 | this.onPrimaryContainer = onPrimaryContainer
137 | return this
138 | }
139 |
140 | fun withSecondary(secondary: Int): Scheme {
141 | this.secondary = secondary
142 | return this
143 | }
144 |
145 | fun withOnSecondary(onSecondary: Int): Scheme {
146 | this.onSecondary = onSecondary
147 | return this
148 | }
149 |
150 | fun withSecondaryContainer(secondaryContainer: Int): Scheme {
151 | this.secondaryContainer = secondaryContainer
152 | return this
153 | }
154 |
155 | fun withOnSecondaryContainer(onSecondaryContainer: Int): Scheme {
156 | this.onSecondaryContainer = onSecondaryContainer
157 | return this
158 | }
159 |
160 | fun withTertiary(tertiary: Int): Scheme {
161 | this.tertiary = tertiary
162 | return this
163 | }
164 |
165 | fun withOnTertiary(onTertiary: Int): Scheme {
166 | this.onTertiary = onTertiary
167 | return this
168 | }
169 |
170 | fun withTertiaryContainer(tertiaryContainer: Int): Scheme {
171 | this.tertiaryContainer = tertiaryContainer
172 | return this
173 | }
174 |
175 | fun withOnTertiaryContainer(onTertiaryContainer: Int): Scheme {
176 | this.onTertiaryContainer = onTertiaryContainer
177 | return this
178 | }
179 |
180 | fun withError(error: Int): Scheme {
181 | this.error = error
182 | return this
183 | }
184 |
185 | fun withOnError(onError: Int): Scheme {
186 | this.onError = onError
187 | return this
188 | }
189 |
190 | fun withErrorContainer(errorContainer: Int): Scheme {
191 | this.errorContainer = errorContainer
192 | return this
193 | }
194 |
195 | fun withOnErrorContainer(onErrorContainer: Int): Scheme {
196 | this.onErrorContainer = onErrorContainer
197 | return this
198 | }
199 |
200 | fun withBackground(background: Int): Scheme {
201 | this.background = background
202 | return this
203 | }
204 |
205 | fun withOnBackground(onBackground: Int): Scheme {
206 | this.onBackground = onBackground
207 | return this
208 | }
209 |
210 | fun withSurface(surface: Int): Scheme {
211 | this.surface = surface
212 | return this
213 | }
214 |
215 | fun withOnSurface(onSurface: Int): Scheme {
216 | this.onSurface = onSurface
217 | return this
218 | }
219 |
220 | fun withSurfaceVariant(surfaceVariant: Int): Scheme {
221 | this.surfaceVariant = surfaceVariant
222 | return this
223 | }
224 |
225 | fun withOnSurfaceVariant(onSurfaceVariant: Int): Scheme {
226 | this.onSurfaceVariant = onSurfaceVariant
227 | return this
228 | }
229 |
230 | fun withOutline(outline: Int): Scheme {
231 | this.outline = outline
232 | return this
233 | }
234 |
235 | fun withOutlineVariant(outlineVariant: Int): Scheme {
236 | this.outlineVariant = outlineVariant
237 | return this
238 | }
239 |
240 | fun withShadow(shadow: Int): Scheme {
241 | this.shadow = shadow
242 | return this
243 | }
244 |
245 | fun withScrim(scrim: Int): Scheme {
246 | this.scrim = scrim
247 | return this
248 | }
249 |
250 | fun withInverseSurface(inverseSurface: Int): Scheme {
251 | this.inverseSurface = inverseSurface
252 | return this
253 | }
254 |
255 | fun withInverseOnSurface(inverseOnSurface: Int): Scheme {
256 | this.inverseOnSurface = inverseOnSurface
257 | return this
258 | }
259 |
260 | fun withInversePrimary(inversePrimary: Int): Scheme {
261 | this.inversePrimary = inversePrimary
262 | return this
263 | }
264 |
265 | override fun toString(): String {
266 | return ("Scheme{"
267 | + "primary="
268 | + primary
269 | + ", onPrimary="
270 | + onPrimary
271 | + ", primaryContainer="
272 | + primaryContainer
273 | + ", onPrimaryContainer="
274 | + onPrimaryContainer
275 | + ", secondary="
276 | + secondary
277 | + ", onSecondary="
278 | + onSecondary
279 | + ", secondaryContainer="
280 | + secondaryContainer
281 | + ", onSecondaryContainer="
282 | + onSecondaryContainer
283 | + ", tertiary="
284 | + tertiary
285 | + ", onTertiary="
286 | + onTertiary
287 | + ", tertiaryContainer="
288 | + tertiaryContainer
289 | + ", onTertiaryContainer="
290 | + onTertiaryContainer
291 | + ", error="
292 | + error
293 | + ", onError="
294 | + onError
295 | + ", errorContainer="
296 | + errorContainer
297 | + ", onErrorContainer="
298 | + onErrorContainer
299 | + ", background="
300 | + background
301 | + ", onBackground="
302 | + onBackground
303 | + ", surface="
304 | + surface
305 | + ", onSurface="
306 | + onSurface
307 | + ", surfaceVariant="
308 | + surfaceVariant
309 | + ", onSurfaceVariant="
310 | + onSurfaceVariant
311 | + ", outline="
312 | + outline
313 | + ", outlineVariant="
314 | + outlineVariant
315 | + ", shadow="
316 | + shadow
317 | + ", scrim="
318 | + scrim
319 | + ", inverseSurface="
320 | + inverseSurface
321 | + ", inverseOnSurface="
322 | + inverseOnSurface
323 | + ", inversePrimary="
324 | + inversePrimary
325 | + '}')
326 | }
327 |
328 | override fun equals(`object`: Any?): Boolean {
329 | if (this === `object`) {
330 | return true
331 | }
332 | if (`object` !is Scheme) {
333 | return false
334 | }
335 | if (!super.equals(`object`)) {
336 | return false
337 | }
338 | val scheme = `object`
339 | if (primary != scheme.primary) {
340 | return false
341 | }
342 | if (onPrimary != scheme.onPrimary) {
343 | return false
344 | }
345 | if (primaryContainer != scheme.primaryContainer) {
346 | return false
347 | }
348 | if (onPrimaryContainer != scheme.onPrimaryContainer) {
349 | return false
350 | }
351 | if (secondary != scheme.secondary) {
352 | return false
353 | }
354 | if (onSecondary != scheme.onSecondary) {
355 | return false
356 | }
357 | if (secondaryContainer != scheme.secondaryContainer) {
358 | return false
359 | }
360 | if (onSecondaryContainer != scheme.onSecondaryContainer) {
361 | return false
362 | }
363 | if (tertiary != scheme.tertiary) {
364 | return false
365 | }
366 | if (onTertiary != scheme.onTertiary) {
367 | return false
368 | }
369 | if (tertiaryContainer != scheme.tertiaryContainer) {
370 | return false
371 | }
372 | if (onTertiaryContainer != scheme.onTertiaryContainer) {
373 | return false
374 | }
375 | if (error != scheme.error) {
376 | return false
377 | }
378 | if (onError != scheme.onError) {
379 | return false
380 | }
381 | if (errorContainer != scheme.errorContainer) {
382 | return false
383 | }
384 | if (onErrorContainer != scheme.onErrorContainer) {
385 | return false
386 | }
387 | if (background != scheme.background) {
388 | return false
389 | }
390 | if (onBackground != scheme.onBackground) {
391 | return false
392 | }
393 | if (surface != scheme.surface) {
394 | return false
395 | }
396 | if (onSurface != scheme.onSurface) {
397 | return false
398 | }
399 | if (surfaceVariant != scheme.surfaceVariant) {
400 | return false
401 | }
402 | if (onSurfaceVariant != scheme.onSurfaceVariant) {
403 | return false
404 | }
405 | if (outline != scheme.outline) {
406 | return false
407 | }
408 | if (outlineVariant != scheme.outlineVariant) {
409 | return false
410 | }
411 | if (shadow != scheme.shadow) {
412 | return false
413 | }
414 | if (scrim != scheme.scrim) {
415 | return false
416 | }
417 | if (inverseSurface != scheme.inverseSurface) {
418 | return false
419 | }
420 | if (inverseOnSurface != scheme.inverseOnSurface) {
421 | return false
422 | }
423 | return if (inversePrimary != scheme.inversePrimary) {
424 | false
425 | } else true
426 | }
427 |
428 | override fun hashCode(): Int {
429 | var result = super.hashCode()
430 | result = 31 * result + primary
431 | result = 31 * result + onPrimary
432 | result = 31 * result + primaryContainer
433 | result = 31 * result + onPrimaryContainer
434 | result = 31 * result + secondary
435 | result = 31 * result + onSecondary
436 | result = 31 * result + secondaryContainer
437 | result = 31 * result + onSecondaryContainer
438 | result = 31 * result + tertiary
439 | result = 31 * result + onTertiary
440 | result = 31 * result + tertiaryContainer
441 | result = 31 * result + onTertiaryContainer
442 | result = 31 * result + error
443 | result = 31 * result + onError
444 | result = 31 * result + errorContainer
445 | result = 31 * result + onErrorContainer
446 | result = 31 * result + background
447 | result = 31 * result + onBackground
448 | result = 31 * result + surface
449 | result = 31 * result + onSurface
450 | result = 31 * result + surfaceVariant
451 | result = 31 * result + onSurfaceVariant
452 | result = 31 * result + outline
453 | result = 31 * result + outlineVariant
454 | result = 31 * result + shadow
455 | result = 31 * result + scrim
456 | result = 31 * result + inverseSurface
457 | result = 31 * result + inverseOnSurface
458 | result = 31 * result + inversePrimary
459 | return result
460 | }
461 |
462 | companion object {
463 | fun light(argb: Int): Scheme {
464 | return lightFromCorePalette(of(argb))
465 | }
466 |
467 | fun dark(argb: Int): Scheme {
468 | return darkFromCorePalette(of(argb))
469 | }
470 |
471 | fun lightContent(argb: Int): Scheme {
472 | return lightFromCorePalette(contentOf(argb))
473 | }
474 |
475 | fun darkContent(argb: Int): Scheme {
476 | return darkFromCorePalette(contentOf(argb))
477 | }
478 |
479 | private fun lightFromCorePalette(core: CorePalette): Scheme {
480 | return Scheme()
481 | .withPrimary(core.a1!!.tone(40))
482 | .withOnPrimary(core.a1!!.tone(100))
483 | .withPrimaryContainer(core.a1!!.tone(90))
484 | .withOnPrimaryContainer(core.a1!!.tone(10))
485 | .withSecondary(core.a2!!.tone(40))
486 | .withOnSecondary(core.a2!!.tone(100))
487 | .withSecondaryContainer(core.a2!!.tone(90))
488 | .withOnSecondaryContainer(core.a2!!.tone(10))
489 | .withTertiary(core.a3!!.tone(40))
490 | .withOnTertiary(core.a3!!.tone(100))
491 | .withTertiaryContainer(core.a3!!.tone(90))
492 | .withOnTertiaryContainer(core.a3!!.tone(10))
493 | .withError(core.error.tone(40))
494 | .withOnError(core.error.tone(100))
495 | .withErrorContainer(core.error.tone(90))
496 | .withOnErrorContainer(core.error.tone(10))
497 | .withBackground(core.n1!!.tone(99))
498 | .withOnBackground(core.n1!!.tone(10))
499 | .withSurface(core.n1!!.tone(99))
500 | .withOnSurface(core.n1!!.tone(10))
501 | .withSurfaceVariant(core.n2!!.tone(90))
502 | .withOnSurfaceVariant(core.n2!!.tone(30))
503 | .withOutline(core.n2!!.tone(50))
504 | .withOutlineVariant(core.n2!!.tone(80))
505 | .withShadow(core.n1!!.tone(0))
506 | .withScrim(core.n1!!.tone(0))
507 | .withInverseSurface(core.n1!!.tone(20))
508 | .withInverseOnSurface(core.n1!!.tone(95))
509 | .withInversePrimary(core.a1!!.tone(80))
510 | }
511 |
512 | private fun darkFromCorePalette(core: CorePalette): Scheme {
513 | return Scheme()
514 | .withPrimary(core.a1!!.tone(80))
515 | .withOnPrimary(core.a1!!.tone(20))
516 | .withPrimaryContainer(core.a1!!.tone(30))
517 | .withOnPrimaryContainer(core.a1!!.tone(90))
518 | .withSecondary(core.a2!!.tone(80))
519 | .withOnSecondary(core.a2!!.tone(20))
520 | .withSecondaryContainer(core.a2!!.tone(30))
521 | .withOnSecondaryContainer(core.a2!!.tone(90))
522 | .withTertiary(core.a3!!.tone(80))
523 | .withOnTertiary(core.a3!!.tone(20))
524 | .withTertiaryContainer(core.a3!!.tone(30))
525 | .withOnTertiaryContainer(core.a3!!.tone(90))
526 | .withError(core.error.tone(80))
527 | .withOnError(core.error.tone(20))
528 | .withErrorContainer(core.error.tone(30))
529 | .withOnErrorContainer(core.error.tone(80))
530 | .withBackground(core.n1!!.tone(10))
531 | .withOnBackground(core.n1!!.tone(90))
532 | .withSurface(core.n1!!.tone(10))
533 | .withOnSurface(core.n1!!.tone(90))
534 | .withSurfaceVariant(core.n2!!.tone(30))
535 | .withOnSurfaceVariant(core.n2!!.tone(80))
536 | .withOutline(core.n2!!.tone(60))
537 | .withOutlineVariant(core.n2!!.tone(30))
538 | .withShadow(core.n1!!.tone(0))
539 | .withScrim(core.n1!!.tone(0))
540 | .withInverseSurface(core.n1!!.tone(90))
541 | .withInverseOnSurface(core.n1!!.tone(20))
542 | .withInversePrimary(core.a1!!.tone(40))
543 | }
544 | }
545 | }
--------------------------------------------------------------------------------
/dynamic_theme/src/main/java/com/t8rin/dynamic/theme/DynamicTheme.kt:
--------------------------------------------------------------------------------
1 | package com.t8rin.dynamic.theme
2 |
3 | import android.Manifest
4 | import android.app.WallpaperManager
5 | import android.content.pm.PackageManager
6 | import android.graphics.Bitmap
7 | import android.graphics.drawable.BitmapDrawable
8 | import android.os.Build
9 | import androidx.annotation.FloatRange
10 | import androidx.compose.animation.animateColorAsState
11 | import androidx.compose.animation.core.AnimationSpec
12 | import androidx.compose.animation.core.tween
13 | import androidx.compose.foundation.background
14 | import androidx.compose.foundation.isSystemInDarkTheme
15 | import androidx.compose.foundation.layout.*
16 | import androidx.compose.foundation.shape.CircleShape
17 | import androidx.compose.material3.*
18 | import androidx.compose.runtime.*
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.clip
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.graphics.compositeOver
24 | import androidx.compose.ui.graphics.toArgb
25 | import androidx.compose.ui.platform.LocalContext
26 | import androidx.compose.ui.platform.LocalDensity
27 | import androidx.compose.ui.platform.LocalLifecycleOwner
28 | import androidx.compose.ui.unit.Density
29 | import androidx.compose.ui.unit.dp
30 | import androidx.core.app.ActivityCompat
31 | import androidx.core.graphics.ColorUtils
32 | import androidx.lifecycle.Lifecycle
33 | import androidx.lifecycle.LifecycleEventObserver
34 | import androidx.palette.graphics.Palette
35 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
36 | import com.t8rin.dynamic.theme.hct.Hct
37 | import com.t8rin.dynamic.theme.palettes.TonalPalette
38 | import com.t8rin.dynamic.theme.scheme.Scheme
39 |
40 | /**
41 | * DynamicTheme allows you to dynamically change the color scheme of the content hierarchy.
42 | * To do this you just need to update [DynamicThemeState].
43 | * @param state - current instance of [DynamicThemeState]
44 | * */
45 | @Composable
46 | fun DynamicTheme(
47 | state: DynamicThemeState,
48 | typography: Typography = Typography(),
49 | density: Density = LocalDensity.current,
50 | defaultColorTuple: ColorTuple,
51 | dynamicColor: Boolean = true,
52 | amoledMode: Boolean = false,
53 | isDarkTheme: Boolean = isSystemInDarkTheme(),
54 | content: @Composable () -> Unit,
55 | ) {
56 | val colorTuple = getAppColorTuple(
57 | defaultColorTuple = defaultColorTuple,
58 | dynamicColor = dynamicColor,
59 | darkTheme = isDarkTheme
60 | )
61 |
62 | LaunchedEffect(colorTuple) {
63 | state.updateColorTuple(colorTuple)
64 | }
65 |
66 | val systemUiController = rememberSystemUiController()
67 | val useDarkIcons = !isDarkTheme
68 |
69 | SideEffect {
70 | systemUiController.setSystemBarsColor(
71 | color = Color.Transparent,
72 | darkIcons = useDarkIcons,
73 | isNavigationBarContrastEnforced = false
74 | )
75 | }
76 |
77 | val scheme = rememberColorScheme(
78 | amoledMode = amoledMode,
79 | isDarkTheme = isDarkTheme,
80 | colorTuple = state.colorTuple.value
81 | ).animateAllColors(tween(150))
82 |
83 | MaterialTheme(
84 | typography = typography,
85 | colorScheme = scheme,
86 | content = {
87 | CompositionLocalProvider(
88 | values = arrayOf(
89 | LocalDynamicThemeState provides state,
90 | LocalDensity provides density
91 | ),
92 | content = content
93 | )
94 | }
95 | )
96 | }
97 |
98 | /**Composable representing ColorTuple object **/
99 | @Composable
100 | fun ColorTupleItem(
101 | modifier: Modifier = Modifier,
102 | backgroundColor: Color = MaterialTheme.colorScheme.surface,
103 | colorTuple: ColorTuple,
104 | content: (@Composable BoxScope.() -> Unit)? = null
105 | ) {
106 | val (primary, secondary, tertiary) = remember(colorTuple) {
107 | derivedStateOf {
108 | colorTuple.run {
109 | val hct = Hct.fromInt(colorTuple.primary.toArgb())
110 | val hue = hct.hue
111 | val chroma = hct.chroma
112 |
113 | val secondary = colorTuple.secondary?.toArgb().let {
114 | if (it != null) {
115 | TonalPalette.fromInt(it)
116 | } else {
117 | TonalPalette.fromHueAndChroma(hue, chroma / 3.0)
118 | }
119 | }
120 | val tertiary = colorTuple.tertiary?.toArgb().let {
121 | if (it != null) {
122 | TonalPalette.fromInt(it)
123 | } else {
124 | TonalPalette.fromHueAndChroma(hue + 60.0, chroma / 2.0)
125 | }
126 | }
127 |
128 | Triple(
129 | primary,
130 | colorTuple.secondary ?: Color(secondary.tone(70)),
131 | colorTuple.tertiary ?: Color(tertiary.tone(70))
132 | )
133 | }
134 | }
135 | }.value.run {
136 | Triple(
137 | animateColorAsState(targetValue = first).value,
138 | animateColorAsState(targetValue = second).value,
139 | animateColorAsState(targetValue = third).value
140 | )
141 | }
142 |
143 | Surface(
144 | modifier = modifier,
145 | color = backgroundColor,
146 | shape = MaterialTheme.shapes.medium,
147 | ) {
148 | Box(
149 | modifier = Modifier
150 | .fillMaxSize()
151 | .padding(8.dp)
152 | .clip(CircleShape),
153 | contentAlignment = Alignment.Center
154 | ) {
155 | Column(
156 | Modifier.fillMaxSize()
157 | ) {
158 | Box(
159 | modifier = Modifier
160 | .fillMaxWidth()
161 | .weight(1f)
162 | .background(primary)
163 | )
164 | Row(
165 | modifier = Modifier
166 | .weight(1f)
167 | .fillMaxWidth()
168 | ) {
169 | Box(
170 | modifier = Modifier
171 | .weight(1f)
172 | .fillMaxHeight()
173 | .background(tertiary)
174 | )
175 | Box(
176 | modifier = Modifier
177 | .weight(1f)
178 | .fillMaxHeight()
179 | .background(secondary)
180 | )
181 | }
182 | }
183 | content?.invoke(this)
184 | }
185 | }
186 | }
187 |
188 | fun Color.calculateSecondaryColor(): Int {
189 | val hct = Hct.fromInt(this.toArgb())
190 | val hue = hct.hue
191 | val chroma = hct.chroma
192 |
193 | return TonalPalette.fromHueAndChroma(hue, chroma / 3.0).tone(80)
194 | }
195 |
196 | fun Color.calculateTertiaryColor(): Int {
197 | val hct = Hct.fromInt(this.toArgb())
198 | val hue = hct.hue
199 | val chroma = hct.chroma
200 |
201 | return TonalPalette.fromHueAndChroma(hue + 60.0, chroma / 2.0).tone(80)
202 | }
203 |
204 | fun Color.calculateSurfaceColor(): Int {
205 | val hct = Hct.fromInt(this.toArgb())
206 | val hue = hct.hue
207 | val chroma = hct.chroma
208 |
209 | return TonalPalette.fromHueAndChroma(hue, (chroma / 12.0).coerceAtMost(4.0)).tone(90)
210 | }
211 |
212 |
213 | @Composable
214 | fun getAppColorTuple(
215 | defaultColorTuple: ColorTuple,
216 | dynamicColor: Boolean,
217 | darkTheme: Boolean
218 | ): ColorTuple {
219 | val context = LocalContext.current
220 | return remember(
221 | LocalLifecycleOwner.current.lifecycle.observeAsState().value,
222 | dynamicColor,
223 | darkTheme,
224 | defaultColorTuple
225 | ) {
226 | derivedStateOf {
227 | var colorTuple: ColorTuple
228 | val wallpaperManager = WallpaperManager.getInstance(context)
229 | val wallColors =
230 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
231 | wallpaperManager
232 | .getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
233 | } else null
234 |
235 | when {
236 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
237 | if (darkTheme) {
238 | dynamicDarkColorScheme(context)
239 | } else {
240 | dynamicLightColorScheme(context)
241 | }.run {
242 | colorTuple = ColorTuple(
243 | primary = primary,
244 | secondary = secondary,
245 | tertiary = tertiary,
246 | surface = surface
247 | )
248 | }
249 | }
250 | dynamicColor && wallColors != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
251 | colorTuple = ColorTuple(
252 | primary = Color(wallColors.primaryColor.toArgb()),
253 | secondary = wallColors.secondaryColor?.toArgb()?.let { Color(it) },
254 | tertiary = wallColors.tertiaryColor?.toArgb()?.let { Color(it) }
255 | )
256 | }
257 | dynamicColor && ActivityCompat.checkSelfPermission(
258 | context,
259 | Manifest.permission.READ_EXTERNAL_STORAGE
260 | ) == PackageManager.PERMISSION_GRANTED -> {
261 | colorTuple = ColorTuple(
262 | primary = (wallpaperManager.drawable as BitmapDrawable).bitmap.extractPrimaryColor()
263 | )
264 | }
265 | else -> {
266 | colorTuple = defaultColorTuple
267 | }
268 | }
269 | colorTuple
270 | }
271 | }.value
272 | }
273 |
274 | @Composable
275 | fun Lifecycle.observeAsState(): State