Add wallpaper color scheme

This commit is contained in:
MM20 2022-04-12 18:28:08 +02:00
parent 408a23563e
commit 8bb3737460
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
25 changed files with 2414 additions and 88 deletions

View File

@ -201,4 +201,12 @@ val OpenSourceLicenses = arrayOf(
licenseText = R.raw.license_apache_2,
url = "https://github.com/promeG/TinyPinyin"
),
OpenSourceLibrary(
name = "material-color-utilities",
copyrightNote = "Copyright 2021 Google LLC",
description = "Algorithms and utilities that power the Material Design 3 (M3) color system, including choosing theme colors from images and creating tones of colors; all in a new color space.",
licenseName = R.string.apache_license_name,
licenseText = R.raw.license_apache_2,
url = "https://github.com/material-foundation/material-color-utilities"
),
)

View File

@ -8,7 +8,6 @@ buildscript {
dependencies {
classpath("com.android.tools.build:gradle:7.1.3")
classpath(libs.kotlin.gradle)
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}

View File

@ -387,6 +387,7 @@
<string name="preference_screen_colors">Color scheme</string>
<string name="preference_colors_default">Default</string>
<string name="preference_colors_bw">Black and White</string>
<string name="preference_colors_wallpaper">From wallpaper</string>
<string name="preference_screen_about">About</string>
<string name="preference_version">Version</string>
<string name="preference_category_links">Links</string>

1
material-color-utilities/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,42 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.koin.android)
}

View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.materialcolorutilities">
</manifest>

View File

@ -0,0 +1,435 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package hct;
import static java.lang.Math.max;
import utils.ColorUtils;
/**
* CAM16, a color appearance model. Colors are not just defined by their hex code, but rather, a hex
* code and viewing conditions.
*
* <p>CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
* astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and should be used when
* measuring distances between colors.
*
* <p>In traditional color spaces, a color can be identified solely by the observer's measurement of
* the color. Color appearance models such as CAM16 also use information about the environment where
* the color was observed, known as the viewing conditions.
*
* <p>For example, white under the traditional assumption of a midday sun white point is accurately
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
*/
public final class Cam16 {
// Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
static final float[][] XYZ_TO_CAM16RGB = {
{0.401288f, 0.650173f, -0.051461f},
{-0.250268f, 1.204414f, 0.045854f},
{-0.002079f, 0.048952f, 0.953127f}
};
// Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
static final float[][] CAM16RGB_TO_XYZ = {
{1.8620678f, -1.0112547f, 0.14918678f},
{0.38752654f, 0.62144744f, -0.00897398f},
{-0.01584150f, -0.03412294f, 1.0499644f}
};
// CAM16 color dimensions, see getters for documentation.
private final float hue;
private final float chroma;
private final float j;
private final float q;
private final float m;
private final float s;
// Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*.
private final float jstar;
private final float astar;
private final float bstar;
/**
* CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
* astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure
* distances between colors.
*/
float distance(Cam16 other) {
float dJ = getJStar() - other.getJStar();
float dA = getAStar() - other.getAStar();
float dB = getBStar() - other.getBStar();
double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB);
double dE = 1.41 * Math.pow(dEPrime, 0.63);
return (float) dE;
}
/** Hue in CAM16 */
public float getHue() {
return hue;
}
/** Chroma in CAM16 */
public float getChroma() {
return chroma;
}
/** Lightness in CAM16 */
public float getJ() {
return j;
}
/**
* Brightness in CAM16.
*
* <p>Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper is
* much brighter viewed in sunlight than in indoor light, but it is the lightest object under any
* lighting.
*/
public float getQ() {
return q;
}
/**
* Colorfulness in CAM16.
*
* <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much
* more colorful outside than inside, but it has the same chroma in both environments.
*/
public float getM() {
return m;
}
/**
* Saturation in CAM16.
*
* <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
* relative to the color's own brightness, where chroma is colorfulness relative to white.
*/
public float getS() {
return s;
}
/** Lightness coordinate in CAM16-UCS */
public float getJStar() {
return jstar;
}
/** a* coordinate in CAM16-UCS */
public float getAStar() {
return astar;
}
/** b* coordinate in CAM16-UCS */
public float getBStar() {
return bstar;
}
/**
* All of the CAM16 dimensions can be calculated from 3 of the dimensions, in the following
* combinations: - {j or q} and {c, m, or s} and hue - jstar, astar, bstar Prefer using a static
* method that constructs from 3 of those dimensions. This constructor is intended for those
* methods to use to return all possible dimensions.
*
* @param hue for example, red, orange, yellow, green, etc.
* @param chroma informally, colorfulness / color intensity. like saturation in HSL, except
* perceptually accurate.
* @param j lightness
* @param q brightness; ratio of lightness to white point's lightness
* @param m colorfulness
* @param s saturation; ratio of chroma to white point's chroma
* @param jstar CAM16-UCS J coordinate
* @param astar CAM16-UCS a coordinate
* @param bstar CAM16-UCS b coordinate
*/
private Cam16(
float hue,
float chroma,
float j,
float q,
float m,
float s,
float jstar,
float astar,
float bstar) {
this.hue = hue;
this.chroma = chroma;
this.j = j;
this.q = q;
this.m = m;
this.s = s;
this.jstar = jstar;
this.astar = astar;
this.bstar = bstar;
}
/**
* Create a CAM16 color from a color, assuming the color was viewed in default viewing conditions.
*
* @param argb ARGB representation of a color.
*/
public static Cam16 fromInt(int argb) {
return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT);
}
/**
* Create a CAM16 color from a color in defined viewing conditions.
*
* @param argb ARGB representation of a color.
* @param viewingConditions Information about the environment where the color was observed.
*/
// The RGB => XYZ conversion matrix elements are derived scientific constants. While the values
// may differ at runtime due to floating point imprecision, keeping the values the same, and
// accurate, across implementations takes precedence.
@SuppressWarnings("FloatingPointLiteralPrecision")
static Cam16 fromIntInViewingConditions(int argb, ViewingConditions viewingConditions) {
// Transform ARGB int to XYZ
int red = (argb & 0x00ff0000) >> 16;
int green = (argb & 0x0000ff00) >> 8;
int blue = (argb & 0x000000ff);
float redL = (float) ColorUtils.linearized(red);
float greenL = (float) ColorUtils.linearized(green);
float blueL = (float) ColorUtils.linearized(blue);
float x = 0.41233895f * redL + 0.35762064f * greenL + 0.18051042f * blueL;
float y = 0.2126f * redL + 0.7152f * greenL + 0.0722f * blueL;
float z = 0.01932141f * redL + 0.11916382f * greenL + 0.95034478f * blueL;
// Transform XYZ to 'cone'/'rgb' responses
float[][] matrix = XYZ_TO_CAM16RGB;
float rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]);
float gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]);
float bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]);
// Discount illuminant
float rD = viewingConditions.getRgbD()[0] * rT;
float gD = viewingConditions.getRgbD()[1] * gT;
float bD = viewingConditions.getRgbD()[2] * bT;
// Chromatic adaptation
float rAF = (float) Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42);
float gAF = (float) Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42);
float bAF = (float) Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42);
float rA = Math.signum(rD) * 400.0f * rAF / (rAF + 27.13f);
float gA = Math.signum(gD) * 400.0f * gAF / (gAF + 27.13f);
float bA = Math.signum(bD) * 400.0f * bAF / (bAF + 27.13f);
// redness-greenness
float a = (float) (11.0 * rA + -12.0 * gA + bA) / 11.0f;
// yellowness-blueness
float b = (float) (rA + gA - 2.0 * bA) / 9.0f;
// auxiliary components
float u = (20.0f * rA + 20.0f * gA + 21.0f * bA) / 20.0f;
float p2 = (40.0f * rA + 20.0f * gA + bA) / 20.0f;
// hue
float atan2 = (float) Math.atan2(b, a);
float atanDegrees = atan2 * 180.0f / (float) Math.PI;
float hue =
atanDegrees < 0
? atanDegrees + 360.0f
: atanDegrees >= 360 ? atanDegrees - 360.0f : atanDegrees;
float hueRadians = hue * (float) Math.PI / 180.0f;
// achromatic response to color
float ac = p2 * viewingConditions.getNbb();
// CAM16 lightness and brightness
float j =
100.0f
* (float)
Math.pow(
ac / viewingConditions.getAw(),
viewingConditions.getC() * viewingConditions.getZ());
float q =
4.0f
/ viewingConditions.getC()
* (float) Math.sqrt(j / 100.0f)
* (viewingConditions.getAw() + 4.0f)
* viewingConditions.getFlRoot();
// CAM16 chroma, colorfulness, and saturation.
float huePrime = (hue < 20.14) ? hue + 360 : hue;
float eHue = 0.25f * (float) (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8);
float p1 = 50000.0f / 13.0f * eHue * viewingConditions.getNc() * viewingConditions.getNcb();
float t = p1 * (float) Math.hypot(a, b) / (u + 0.305f);
float alpha =
(float) Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73)
* (float) Math.pow(t, 0.9);
// CAM16 chroma, colorfulness, saturation
float c = alpha * (float) Math.sqrt(j / 100.0);
float m = c * viewingConditions.getFlRoot();
float s =
50.0f
* (float)
Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0f));
// CAM16-UCS components
float jstar = (1.0f + 100.0f * 0.007f) * j / (1.0f + 0.007f * j);
float mstar = 1.0f / 0.0228f * (float) Math.log1p(0.0228f * m);
float astar = mstar * (float) Math.cos(hueRadians);
float bstar = mstar * (float) Math.sin(hueRadians);
return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar);
}
/**
* @param j CAM16 lightness
* @param c CAM16 chroma
* @param h CAM16 hue
*/
static Cam16 fromJch(float j, float c, float h) {
return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT);
}
/**
* @param j CAM16 lightness
* @param c CAM16 chroma
* @param h CAM16 hue
* @param viewingConditions Information about the environment where the color was observed.
*/
private static Cam16 fromJchInViewingConditions(
float j, float c, float h, ViewingConditions viewingConditions) {
float q =
4.0f
/ viewingConditions.getC()
* (float) Math.sqrt(j / 100.0)
* (viewingConditions.getAw() + 4.0f)
* viewingConditions.getFlRoot();
float m = c * viewingConditions.getFlRoot();
float alpha = c / (float) Math.sqrt(j / 100.0);
float s =
50.0f
* (float)
Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0f));
float hueRadians = h * (float) Math.PI / 180.0f;
float jstar = (1.0f + 100.0f * 0.007f) * j / (1.0f + 0.007f * j);
float mstar = 1.0f / 0.0228f * (float) Math.log1p(0.0228 * m);
float astar = mstar * (float) Math.cos(hueRadians);
float bstar = mstar * (float) Math.sin(hueRadians);
return new Cam16(h, c, j, q, m, s, jstar, astar, bstar);
}
/**
* Create a CAM16 color from CAM16-UCS coordinates.
*
* @param jstar CAM16-UCS lightness.
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
* axis.
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
* axis.
*/
public static Cam16 fromUcs(float jstar, float astar, float bstar) {
return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT);
}
/**
* Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions.
*
* @param jstar CAM16-UCS lightness.
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
* axis.
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
* axis.
* @param viewingConditions Information about the environment where the color was observed.
*/
public static Cam16 fromUcsInViewingConditions(
float jstar, float astar, float bstar, ViewingConditions viewingConditions) {
double m = Math.hypot(astar, bstar);
double m2 = Math.expm1(m * 0.0228f) / 0.0228f;
double c = m2 / viewingConditions.getFlRoot();
double h = Math.atan2(bstar, astar) * (180.0f / Math.PI);
if (h < 0.0) {
h += 360.0f;
}
float j = jstar / (1f - (jstar - 100f) * 0.007f);
return fromJchInViewingConditions(j, (float) c, (float) h, viewingConditions);
}
/**
* ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
* which are near-identical to the default viewing conditions for sRGB.
*/
public int getInt() {
return viewed(ViewingConditions.DEFAULT);
}
/**
* ARGB representation of the color, in defined viewing conditions.
*
* @param viewingConditions Information about the environment where the color will be viewed.
* @return ARGB representation of color
*/
int viewed(ViewingConditions viewingConditions) {
float alpha =
(getChroma() == 0.0 || getJ() == 0.0)
? 0.0f
: getChroma() / (float) Math.sqrt(getJ() / 100.0);
float t =
(float)
Math.pow(
alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9);
float hRad = getHue() * (float) Math.PI / 180.0f;
float eHue = 0.25f * (float) (Math.cos(hRad + 2.0) + 3.8);
float ac =
viewingConditions.getAw()
* (float)
Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
float p1 = eHue * (50000.0f / 13.0f) * viewingConditions.getNc() * viewingConditions.getNcb();
float p2 = (ac / viewingConditions.getNbb());
float hSin = (float) Math.sin(hRad);
float hCos = (float) Math.cos(hRad);
float gamma = 23.0f * (p2 + 0.305f) * t / (23.0f * p1 + 11.0f * t * hCos + 108.0f * t * hSin);
float a = gamma * hCos;
float b = gamma * hSin;
float rA = (460.0f * p2 + 451.0f * a + 288.0f * b) / 1403.0f;
float gA = (460.0f * p2 - 891.0f * a - 261.0f * b) / 1403.0f;
float bA = (460.0f * p2 - 220.0f * a - 6300.0f * b) / 1403.0f;
float rCBase = (float) max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
float rC =
Math.signum(rA)
* (100.0f / viewingConditions.getFl())
* (float) Math.pow(rCBase, 1.0 / 0.42);
float gCBase = (float) max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
float gC =
Math.signum(gA)
* (100.0f / viewingConditions.getFl())
* (float) Math.pow(gCBase, 1.0 / 0.42);
float bCBase = (float) max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
float bC =
Math.signum(bA)
* (100.0f / viewingConditions.getFl())
* (float) Math.pow(bCBase, 1.0 / 0.42);
float rF = rC / viewingConditions.getRgbD()[0];
float gF = gC / viewingConditions.getRgbD()[1];
float bF = bC / viewingConditions.getRgbD()[2];
float[][] matrix = CAM16RGB_TO_XYZ;
float x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]);
float y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]);
float z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);
return ColorUtils.argbFromXyz(x, y, z);
}
}

View File

@ -0,0 +1,262 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package hct;
import utils.ColorUtils;
import utils.MathUtils;
/**
* A color system built using CAM16 hue and chroma, and L* from L*a*b*.
*
* <p>Using L* creates a link between the color system, contrast, and thus accessibility. Contrast
* ratio depends on relative luminance, or Y in the XYZ color space. L*, or perceptual luminance can
* be calculated from Y.
*
* <p>Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones.
*
* <p>Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate. A
* difference of 40 in HCT tone guarantees a contrast ratio >= 3.0, and a difference of 50
* guarantees a contrast ratio >= 4.5.
*/
/**
* HCT, hue, chroma, and tone. A color system that provides a perceptually accurate color
* measurement system that can also accurately render what colors will appear as in different
* lighting environments.
*/
public final class Hct {
private float hue;
private float chroma;
private float tone;
/**
* Create an HCT color from hue, chroma, and tone.
*
* @param hue 0 <= hue < 360; invalid values are corrected.
* @param chroma 0 <= chroma < ?; Informally, colorfulness. The color returned may be lower than
* the requested chroma. Chroma has a different maximum for any given hue and tone.
* @param tone 0 <= tone <= 100; invalid values are corrected.
* @return HCT representation of a color in default viewing conditions.
*/
public static Hct from(float hue, float chroma, float tone) {
return new Hct(hue, chroma, tone);
}
/**
* Create an HCT color from a color.
*
* @param argb ARGB representation of a color.
* @return HCT representation of a color in default viewing conditions
*/
public static Hct fromInt(int argb) {
Cam16 cam = Cam16.fromInt(argb);
return new Hct(cam.getHue(), cam.getChroma(), (float) ColorUtils.lstarFromArgb(argb));
}
private Hct(float hue, float chroma, float tone) {
setInternalState(gamutMap(hue, chroma, tone));
}
public float getHue() {
return hue;
}
public float getChroma() {
return chroma;
}
public float getTone() {
return tone;
}
public int toInt() {
return gamutMap(hue, chroma, tone);
}
/**
* Set the hue of this color. Chroma may decrease because chroma has a different maximum for any
* given hue and tone.
*
* @param newHue 0 <= newHue < 360; invalid values are corrected.
*/
public void setHue(float newHue) {
setInternalState(gamutMap((float) MathUtils.sanitizeDegreesDouble(newHue), chroma, tone));
}
/**
* Set the chroma of this color. Chroma may decrease because chroma has a different maximum for
* any given hue and tone.
*
* @param newChroma 0 <= newChroma < ?
*/
public void setChroma(float newChroma) {
setInternalState(gamutMap(hue, newChroma, tone));
}
/**
* Set the tone of this color. Chroma may decrease because chroma has a different maximum for any
* given hue and tone.
*
* @param newTone 0 <= newTone <= 100; invalid valids are corrected.
*/
public void setTone(float newTone) {
setInternalState(gamutMap(hue, chroma, newTone));
}
private void setInternalState(int argb) {
Cam16 cam = Cam16.fromInt(argb);
float tone = (float) ColorUtils.lstarFromArgb(argb);
hue = cam.getHue();
chroma = cam.getChroma();
this.tone = tone;
}
/**
* When the delta between the floor & ceiling of a binary search for maximum chroma at a hue and
* tone is less than this, the binary search terminates.
*/
private static final float CHROMA_SEARCH_ENDPOINT = 0.4f;
/** The maximum color distance, in CAM16-UCS, between a requested color and the color returned. */
private static final float DE_MAX = 1.0f;
/** The maximum difference between the requested L* and the L* returned. */
private static final float DL_MAX = 0.2f;
/**
* The minimum color distance, in CAM16-UCS, between a requested color and an 'exact' match. This
* allows the binary search during gamut mapping to terminate much earlier when the error is
* infinitesimal.
*/
private static final float DE_MAX_ERROR = 0.000000001f;
/**
* When the delta between the floor & ceiling of a binary search for J, lightness in CAM16, is
* less than this, the binary search terminates.
*/
private static final float LIGHTNESS_SEARCH_ENDPOINT = 0.01f;
/**
* @param hue a number, in degrees, representing ex. red, orange, yellow, etc. Ranges from 0 <=
* hue < 360.
* @param chroma Informally, colorfulness. Ranges from 0 to roughly 150. Like all perceptually
* accurate color systems, chroma has a different maximum for any given hue and tone, so the
* color returned may be lower than the requested chroma.
* @param tone Lightness. Ranges from 0 to 100.
* @return ARGB representation of a color in default viewing conditions
*/
private static int gamutMap(float hue, float chroma, float tone) {
return gamutMapInViewingConditions(hue, chroma, tone, ViewingConditions.DEFAULT);
}
/**
* @param hue CAM16 hue.
* @param chroma CAM16 chroma.
* @param tone L*a*b* lightness.
* @param viewingConditions Information about the environment where the color was observed.
*/
static int gamutMapInViewingConditions(
float hue, float chroma, float tone, ViewingConditions viewingConditions) {
if (chroma < 1.0 || Math.round(tone) <= 0.0 || Math.round(tone) >= 100.0) {
return ColorUtils.argbFromLstar(tone);
}
hue = (float) MathUtils.sanitizeDegreesDouble(hue);
float high = chroma;
float mid = chroma;
float low = 0.0f;
boolean isFirstLoop = true;
Cam16 answer = null;
while (Math.abs(low - high) >= CHROMA_SEARCH_ENDPOINT) {
Cam16 possibleAnswer = findCamByJ(hue, mid, tone);
if (isFirstLoop) {
if (possibleAnswer != null) {
return possibleAnswer.viewed(viewingConditions);
} else {
isFirstLoop = false;
mid = low + (high - low) / 2.0f;
continue;
}
}
if (possibleAnswer == null) {
high = mid;
} else {
answer = possibleAnswer;
low = mid;
}
mid = low + (high - low) / 2.0f;
}
if (answer == null) {
return ColorUtils.argbFromLstar(tone);
}
return answer.viewed(viewingConditions);
}
/**
* @param hue CAM16 hue
* @param chroma CAM16 chroma
* @param tone L*a*b* lightness
* @return CAM16 instance within error tolerance of the provided dimensions, or null.
*/
private static Cam16 findCamByJ(float hue, float chroma, float tone) {
float low = 0.0f;
float high = 100.0f;
float mid = 0.0f;
float bestdL = 1000.0f;
float bestdE = 1000.0f;
Cam16 bestCam = null;
while (Math.abs(low - high) > LIGHTNESS_SEARCH_ENDPOINT) {
mid = low + (high - low) / 2;
Cam16 camBeforeClip = Cam16.fromJch(mid, chroma, hue);
int clipped = camBeforeClip.getInt();
float clippedLstar = (float) ColorUtils.lstarFromArgb(clipped);
float dL = Math.abs(tone - clippedLstar);
if (dL < DL_MAX) {
Cam16 camClipped = Cam16.fromInt(clipped);
float dE =
camClipped.distance(Cam16.fromJch(camClipped.getJ(), camClipped.getChroma(), hue));
if (dE <= DE_MAX && dE <= bestdE) {
bestdL = dL;
bestdE = dE;
bestCam = camClipped;
}
}
if (bestdL == 0 && bestdE < DE_MAX_ERROR) {
break;
}
if (clippedLstar < tone) {
low = mid;
} else {
high = mid;
}
}
return bestCam;
}
}

View File

@ -0,0 +1,198 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package hct;
import utils.ColorUtils;
import utils.MathUtils;
/**
* In traditional color spaces, a color can be identified solely by the observer's measurement of
* the color. Color appearance models such as CAM16 also use information about the environment where
* the color was observed, known as the viewing conditions.
*
* <p>For example, white under the traditional assumption of a midday sun white point is accurately
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
*
* <p>This class caches intermediate values of the CAM16 conversion process that depend only on
* viewing conditions, enabling speed ups.
*/
public final class ViewingConditions {
/** sRGB-like viewing conditions. */
public static final ViewingConditions DEFAULT =
ViewingConditions.make(
new float[] {
(float) ColorUtils.whitePointD65()[0],
(float) ColorUtils.whitePointD65()[1],
(float) ColorUtils.whitePointD65()[2]
},
(float) (200.0f / Math.PI * ColorUtils.yFromLstar(50.0f) / 100.f),
50.0f,
2.0f,
false);
private final float aw;
private final float nbb;
private final float ncb;
private final float c;
private final float nc;
private final float n;
private final float[] rgbD;
private final float fl;
private final float flRoot;
private final float z;
public float getAw() {
return aw;
}
public float getN() {
return n;
}
public float getNbb() {
return nbb;
}
float getNcb() {
return ncb;
}
float getC() {
return c;
}
float getNc() {
return nc;
}
public float[] getRgbD() {
return rgbD;
}
float getFl() {
return fl;
}
public float getFlRoot() {
return flRoot;
}
float getZ() {
return z;
}
/**
* Create ViewingConditions from a simple, physically relevant, set of parameters.
*
* @param whitePoint White point, measured in the XYZ color space. default = D65, or sunny day
* afternoon
* @param adaptingLuminance The luminance of the adapting field. Informally, how bright it is in
* the room where the color is viewed. Can be calculated from lux by multiplying lux by
* 0.0586. default = 11.72, or 200 lux.
* @param backgroundLstar The lightness of the area surrounding the color. measured by L* in
* L*a*b*. default = 50.0
* @param surround A general description of the lighting surrounding the color. 0 is pitch dark,
* like watching a movie in a theater. 1.0 is a dimly light room, like watching TV at home at
* night. 2.0 means there is no difference between the lighting on the color and around it.
* default = 2.0
* @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting,
* such as knowing an apple is still red in green light. default = false, the eye does not
* perform this process on self-luminous objects like displays.
*/
static ViewingConditions make(
float[] whitePoint,
float adaptingLuminance,
float backgroundLstar,
float surround,
boolean discountingIlluminant) {
// Transform white point XYZ to 'cone'/'rgb' responses
float[][] matrix = Cam16.XYZ_TO_CAM16RGB;
float[] xyz = whitePoint;
float rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
float gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
float bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
float f = 0.8f + (surround / 10.0f);
float c =
(f >= 0.9)
? (float) MathUtils.lerp(0.59f, 0.69f, ((f - 0.9f) * 10.0f))
: (float) MathUtils.lerp(0.525f, 0.59f, ((f - 0.8f) * 10.0f));
float d =
discountingIlluminant
? 1.0f
: f * (1.0f - ((1.0f / 3.6f) * (float) Math.exp((-adaptingLuminance - 42.0f) / 92.0f)));
d = (d > 1.0) ? 1.0f : (d < 0.0) ? 0.0f : d;
float nc = f;
float[] rgbD =
new float[] {
d * (100.0f / rW) + 1.0f - d, d * (100.0f / gW) + 1.0f - d, d * (100.0f / bW) + 1.0f - d
};
float k = 1.0f / (5.0f * adaptingLuminance + 1.0f);
float k4 = k * k * k * k;
float k4F = 1.0f - k4;
float fl =
(k4 * adaptingLuminance) + (0.1f * k4F * k4F * (float) Math.cbrt(5.0 * adaptingLuminance));
float n = (float) (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]);
float z = 1.48f + (float) Math.sqrt(n);
float nbb = 0.725f / (float) Math.pow(n, 0.2);
float ncb = nbb;
float[] rgbAFactors =
new float[] {
(float) Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
(float) Math.pow(fl * rgbD[1] * gW / 100.0, 0.42),
(float) Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
};
float[] rgbA =
new float[] {
(400.0f * rgbAFactors[0]) / (rgbAFactors[0] + 27.13f),
(400.0f * rgbAFactors[1]) / (rgbAFactors[1] + 27.13f),
(400.0f * rgbAFactors[2]) / (rgbAFactors[2] + 27.13f)
};
float aw = ((2.0f * rgbA[0]) + rgbA[1] + (0.05f * rgbA[2])) * nbb;
return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, (float) Math.pow(fl, 0.25), z);
}
/**
* Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand
* for technical color science terminology, this class would not benefit from documenting them
* individually. A brief overview is available in the CAM16 specification, and a complete overview
* requires a color science textbook, such as Fairchild's Color Appearance Models.
*/
private ViewingConditions(
float n,
float aw,
float nbb,
float ncb,
float c,
float nc,
float[] rgbD,
float fl,
float flRoot,
float z) {
this.n = n;
this.aw = aw;
this.nbb = nbb;
this.ncb = ncb;
this.c = c;
this.nc = nc;
this.rgbD = rgbD;
this.fl = fl;
this.flRoot = flRoot;
this.z = z;
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package palettes;
import static java.lang.Math.max;
import hct.Hct;
/**
* An intermediate concept between the key color for a UI theme, and a full color scheme. 5 sets of
* tones are generated, all except one use the same hue as the key color, and all vary in chroma.
*/
public final class CorePalette {
public TonalPalette a1;
public TonalPalette a2;
public TonalPalette a3;
public TonalPalette n1;
public TonalPalette n2;
public TonalPalette error;
/**
* Create key tones from a color.
*
* @param argb ARGB representation of a color
*/
public static CorePalette of(int argb) {
return new CorePalette(argb);
}
private CorePalette(int argb) {
Hct hct = Hct.fromInt(argb);
float hue = hct.getHue();
this.a1 = TonalPalette.fromHueAndChroma(hue, max(48f, hct.getChroma()));
this.a2 = TonalPalette.fromHueAndChroma(hue, 16f);
this.a3 = TonalPalette.fromHueAndChroma(hue + 60f, 24f);
this.n1 = TonalPalette.fromHueAndChroma(hue, 4f);
this.n2 = TonalPalette.fromHueAndChroma(hue, 8f);
this.error = TonalPalette.fromHueAndChroma(25, 84f);
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package palettes;
import hct.Hct;
import java.util.HashMap;
import java.util.Map;
/**
* A convenience class for retrieving colors that are constant in hue and chroma, but vary in tone.
*/
public final class TonalPalette {
Map<Integer, Integer> cache;
float hue;
float chroma;
/**
* Create tones using the HCT hue and chroma from a color.
*
* @param argb ARGB representation of a color
* @return Tones matching that color's hue and chroma.
*/
public static final TonalPalette fromInt(int argb) {
Hct hct = Hct.fromInt(argb);
return TonalPalette.fromHueAndChroma(hct.getHue(), hct.getChroma());
}
/**
* Create tones from a defined HCT hue and chroma.
*
* @param hue HCT hue
* @param chroma HCT chroma
* @return Tones matching hue and chroma.
*/
public static final TonalPalette fromHueAndChroma(float hue, float chroma) {
return new TonalPalette(hue, chroma);
}
private TonalPalette(float hue, float chroma) {
cache = new HashMap<>();
this.hue = hue;
this.chroma = chroma;
}
/**
* Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT tone.
*
* @param tone HCT tone, measured from 0 to 100.
* @return ARGB representation of a color with that tone.
*/
// AndroidJdkLibsChecker is higher priority than ComputeIfAbsentUseValue (b/119581923)
@SuppressWarnings("ComputeIfAbsentUseValue")
public int tone(int tone) {
Integer color = cache.get(tone);
if (color == null) {
color = Hct.from(this.hue, this.chroma, tone).toInt();
cache.put(tone, color);
}
return color;
}
}

View File

@ -0,0 +1,717 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package scheme;
import palettes.CorePalette;
/** Represents a Material color scheme, a mapping of color roles to colors. */
public class Scheme {
private int primary;
private int onPrimary;
private int primaryContainer;
private int onPrimaryContainer;
private int secondary;
private int onSecondary;
private int secondaryContainer;
private int onSecondaryContainer;
private int tertiary;
private int onTertiary;
private int tertiaryContainer;
private int onTertiaryContainer;
private int error;
private int onError;
private int errorContainer;
private int onErrorContainer;
private int background;
private int onBackground;
private int surface;
private int onSurface;
private int surfaceVariant;
private int onSurfaceVariant;
private int outline;
private int shadow;
private int inverseSurface;
private int inverseOnSurface;
private int inversePrimary;
public Scheme() {}
public Scheme(
int primary,
int onPrimary,
int primaryContainer,
int onPrimaryContainer,
int secondary,
int onSecondary,
int secondaryContainer,
int onSecondaryContainer,
int tertiary,
int onTertiary,
int tertiaryContainer,
int onTertiaryContainer,
int error,
int onError,
int errorContainer,
int onErrorContainer,
int background,
int onBackground,
int surface,
int onSurface,
int surfaceVariant,
int onSurfaceVariant,
int outline,
int shadow,
int inverseSurface,
int inverseOnSurface,
int inversePrimary) {
super();
this.primary = primary;
this.onPrimary = onPrimary;
this.primaryContainer = primaryContainer;
this.onPrimaryContainer = onPrimaryContainer;
this.secondary = secondary;
this.onSecondary = onSecondary;
this.secondaryContainer = secondaryContainer;
this.onSecondaryContainer = onSecondaryContainer;
this.tertiary = tertiary;
this.onTertiary = onTertiary;
this.tertiaryContainer = tertiaryContainer;
this.onTertiaryContainer = onTertiaryContainer;
this.error = error;
this.onError = onError;
this.errorContainer = errorContainer;
this.onErrorContainer = onErrorContainer;
this.background = background;
this.onBackground = onBackground;
this.surface = surface;
this.onSurface = onSurface;
this.surfaceVariant = surfaceVariant;
this.onSurfaceVariant = onSurfaceVariant;
this.outline = outline;
this.shadow = shadow;
this.inverseSurface = inverseSurface;
this.inverseOnSurface = inverseOnSurface;
this.inversePrimary = inversePrimary;
}
public static Scheme light(int argb) {
CorePalette core = CorePalette.of(argb);
return new Scheme()
.withPrimary(core.a1.tone(40))
.withOnPrimary(core.a1.tone(100))
.withPrimaryContainer(core.a1.tone(90))
.withOnPrimaryContainer(core.a1.tone(10))
.withSecondary(core.a2.tone(40))
.withOnSecondary(core.a2.tone(100))
.withSecondaryContainer(core.a2.tone(90))
.withOnSecondaryContainer(core.a2.tone(10))
.withTertiary(core.a3.tone(40))
.withOnTertiary(core.a3.tone(100))
.withTertiaryContainer(core.a3.tone(90))
.withOnTertiaryContainer(core.a3.tone(10))
.withError(core.error.tone(40))
.withOnError(core.error.tone(100))
.withErrorContainer(core.error.tone(90))
.withOnErrorContainer(core.error.tone(10))
.withBackground(core.n1.tone(99))
.withOnBackground(core.n1.tone(10))
.withSurface(core.n1.tone(99))
.withOnSurface(core.n1.tone(10))
.withSurfaceVariant(core.n2.tone(90))
.withOnSurfaceVariant(core.n2.tone(30))
.withOutline(core.n2.tone(50))
.withShadow(core.n1.tone(0))
.withInverseSurface(core.n1.tone(20))
.withInverseOnSurface(core.n1.tone(95))
.withInversePrimary(core.a1.tone(80));
}
public static Scheme dark(int argb) {
CorePalette core = CorePalette.of(argb);
return new Scheme()
.withPrimary(core.a1.tone(80))
.withOnPrimary(core.a1.tone(20))
.withPrimaryContainer(core.a1.tone(30))
.withOnPrimaryContainer(core.a1.tone(90))
.withSecondary(core.a2.tone(80))
.withOnSecondary(core.a2.tone(20))
.withSecondaryContainer(core.a2.tone(30))
.withOnSecondaryContainer(core.a2.tone(90))
.withTertiary(core.a3.tone(80))
.withOnTertiary(core.a3.tone(20))
.withTertiaryContainer(core.a3.tone(30))
.withOnTertiaryContainer(core.a3.tone(90))
.withError(core.error.tone(80))
.withOnError(core.error.tone(20))
.withErrorContainer(core.error.tone(30))
.withOnErrorContainer(core.error.tone(80))
.withBackground(core.n1.tone(10))
.withOnBackground(core.n1.tone(90))
.withSurface(core.n1.tone(10))
.withOnSurface(core.n1.tone(90))
.withSurfaceVariant(core.n2.tone(30))
.withOnSurfaceVariant(core.n2.tone(80))
.withOutline(core.n2.tone(60))
.withShadow(core.n1.tone(0))
.withInverseSurface(core.n1.tone(90))
.withInverseOnSurface(core.n1.tone(20))
.withInversePrimary(core.a1.tone(40));
}
public int getPrimary() {
return primary;
}
public void setPrimary(int primary) {
this.primary = primary;
}
public Scheme withPrimary(int primary) {
this.primary = primary;
return this;
}
public int getOnPrimary() {
return onPrimary;
}
public void setOnPrimary(int onPrimary) {
this.onPrimary = onPrimary;
}
public Scheme withOnPrimary(int onPrimary) {
this.onPrimary = onPrimary;
return this;
}
public int getPrimaryContainer() {
return primaryContainer;
}
public void setPrimaryContainer(int primaryContainer) {
this.primaryContainer = primaryContainer;
}
public Scheme withPrimaryContainer(int primaryContainer) {
this.primaryContainer = primaryContainer;
return this;
}
public int getOnPrimaryContainer() {
return onPrimaryContainer;
}
public void setOnPrimaryContainer(int onPrimaryContainer) {
this.onPrimaryContainer = onPrimaryContainer;
}
public Scheme withOnPrimaryContainer(int onPrimaryContainer) {
this.onPrimaryContainer = onPrimaryContainer;
return this;
}
public int getSecondary() {
return secondary;
}
public void setSecondary(int secondary) {
this.secondary = secondary;
}
public Scheme withSecondary(int secondary) {
this.secondary = secondary;
return this;
}
public int getOnSecondary() {
return onSecondary;
}
public void setOnSecondary(int onSecondary) {
this.onSecondary = onSecondary;
}
public Scheme withOnSecondary(int onSecondary) {
this.onSecondary = onSecondary;
return this;
}
public int getSecondaryContainer() {
return secondaryContainer;
}
public void setSecondaryContainer(int secondaryContainer) {
this.secondaryContainer = secondaryContainer;
}
public Scheme withSecondaryContainer(int secondaryContainer) {
this.secondaryContainer = secondaryContainer;
return this;
}
public int getOnSecondaryContainer() {
return onSecondaryContainer;
}
public void setOnSecondaryContainer(int onSecondaryContainer) {
this.onSecondaryContainer = onSecondaryContainer;
}
public Scheme withOnSecondaryContainer(int onSecondaryContainer) {
this.onSecondaryContainer = onSecondaryContainer;
return this;
}
public int getTertiary() {
return tertiary;
}
public void setTertiary(int tertiary) {
this.tertiary = tertiary;
}
public Scheme withTertiary(int tertiary) {
this.tertiary = tertiary;
return this;
}
public int getOnTertiary() {
return onTertiary;
}
public void setOnTertiary(int onTertiary) {
this.onTertiary = onTertiary;
}
public Scheme withOnTertiary(int onTertiary) {
this.onTertiary = onTertiary;
return this;
}
public int getTertiaryContainer() {
return tertiaryContainer;
}
public void setTertiaryContainer(int tertiaryContainer) {
this.tertiaryContainer = tertiaryContainer;
}
public Scheme withTertiaryContainer(int tertiaryContainer) {
this.tertiaryContainer = tertiaryContainer;
return this;
}
public int getOnTertiaryContainer() {
return onTertiaryContainer;
}
public void setOnTertiaryContainer(int onTertiaryContainer) {
this.onTertiaryContainer = onTertiaryContainer;
}
public Scheme withOnTertiaryContainer(int onTertiaryContainer) {
this.onTertiaryContainer = onTertiaryContainer;
return this;
}
public int getError() {
return error;
}
public void setError(int error) {
this.error = error;
}
public Scheme withError(int error) {
this.error = error;
return this;
}
public int getOnError() {
return onError;
}
public void setOnError(int onError) {
this.onError = onError;
}
public Scheme withOnError(int onError) {
this.onError = onError;
return this;
}
public int getErrorContainer() {
return errorContainer;
}
public void setErrorContainer(int errorContainer) {
this.errorContainer = errorContainer;
}
public Scheme withErrorContainer(int errorContainer) {
this.errorContainer = errorContainer;
return this;
}
public int getOnErrorContainer() {
return onErrorContainer;
}
public void setOnErrorContainer(int onErrorContainer) {
this.onErrorContainer = onErrorContainer;
}
public Scheme withOnErrorContainer(int onErrorContainer) {
this.onErrorContainer = onErrorContainer;
return this;
}
public int getBackground() {
return background;
}
public void setBackground(int background) {
this.background = background;
}
public Scheme withBackground(int background) {
this.background = background;
return this;
}
public int getOnBackground() {
return onBackground;
}
public void setOnBackground(int onBackground) {
this.onBackground = onBackground;
}
public Scheme withOnBackground(int onBackground) {
this.onBackground = onBackground;
return this;
}
public int getSurface() {
return surface;
}
public void setSurface(int surface) {
this.surface = surface;
}
public Scheme withSurface(int surface) {
this.surface = surface;
return this;
}
public int getOnSurface() {
return onSurface;
}
public void setOnSurface(int onSurface) {
this.onSurface = onSurface;
}
public Scheme withOnSurface(int onSurface) {
this.onSurface = onSurface;
return this;
}
public int getSurfaceVariant() {
return surfaceVariant;
}
public void setSurfaceVariant(int surfaceVariant) {
this.surfaceVariant = surfaceVariant;
}
public Scheme withSurfaceVariant(int surfaceVariant) {
this.surfaceVariant = surfaceVariant;
return this;
}
public int getOnSurfaceVariant() {
return onSurfaceVariant;
}
public void setOnSurfaceVariant(int onSurfaceVariant) {
this.onSurfaceVariant = onSurfaceVariant;
}
public Scheme withOnSurfaceVariant(int onSurfaceVariant) {
this.onSurfaceVariant = onSurfaceVariant;
return this;
}
public int getOutline() {
return outline;
}
public void setOutline(int outline) {
this.outline = outline;
}
public Scheme withOutline(int outline) {
this.outline = outline;
return this;
}
public int getShadow() {
return shadow;
}
public void setShadow(int shadow) {
this.shadow = shadow;
}
public Scheme withShadow(int shadow) {
this.shadow = shadow;
return this;
}
public int getInverseSurface() {
return inverseSurface;
}
public void setInverseSurface(int inverseSurface) {
this.inverseSurface = inverseSurface;
}
public Scheme withInverseSurface(int inverseSurface) {
this.inverseSurface = inverseSurface;
return this;
}
public int getInverseOnSurface() {
return inverseOnSurface;
}
public void setInverseOnSurface(int inverseOnSurface) {
this.inverseOnSurface = inverseOnSurface;
}
public Scheme withInverseOnSurface(int inverseOnSurface) {
this.inverseOnSurface = inverseOnSurface;
return this;
}
public int getInversePrimary() {
return inversePrimary;
}
public void setInversePrimary(int inversePrimary) {
this.inversePrimary = inversePrimary;
}
public Scheme withInversePrimary(int inversePrimary) {
this.inversePrimary = inversePrimary;
return this;
}
@Override
public String toString() {
return "Scheme{"
+ "primary="
+ primary
+ ", onPrimary="
+ onPrimary
+ ", primaryContainer="
+ primaryContainer
+ ", onPrimaryContainer="
+ onPrimaryContainer
+ ", secondary="
+ secondary
+ ", onSecondary="
+ onSecondary
+ ", secondaryContainer="
+ secondaryContainer
+ ", onSecondaryContainer="
+ onSecondaryContainer
+ ", tertiary="
+ tertiary
+ ", onTertiary="
+ onTertiary
+ ", tertiaryContainer="
+ tertiaryContainer
+ ", onTertiaryContainer="
+ onTertiaryContainer
+ ", error="
+ error
+ ", onError="
+ onError
+ ", errorContainer="
+ errorContainer
+ ", onErrorContainer="
+ onErrorContainer
+ ", background="
+ background
+ ", onBackground="
+ onBackground
+ ", surface="
+ surface
+ ", onSurface="
+ onSurface
+ ", surfaceVariant="
+ surfaceVariant
+ ", onSurfaceVariant="
+ onSurfaceVariant
+ ", outline="
+ outline
+ ", shadow="
+ shadow
+ ", inverseSurface="
+ inverseSurface
+ ", inverseOnSurface="
+ inverseOnSurface
+ ", inversePrimary="
+ inversePrimary
+ '}';
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof Scheme)) {
return false;
}
if (!super.equals(object)) {
return false;
}
Scheme scheme = (Scheme) object;
if (primary != scheme.primary) {
return false;
}
if (onPrimary != scheme.onPrimary) {
return false;
}
if (primaryContainer != scheme.primaryContainer) {
return false;
}
if (onPrimaryContainer != scheme.onPrimaryContainer) {
return false;
}
if (secondary != scheme.secondary) {
return false;
}
if (onSecondary != scheme.onSecondary) {
return false;
}
if (secondaryContainer != scheme.secondaryContainer) {
return false;
}
if (onSecondaryContainer != scheme.onSecondaryContainer) {
return false;
}
if (tertiary != scheme.tertiary) {
return false;
}
if (onTertiary != scheme.onTertiary) {
return false;
}
if (tertiaryContainer != scheme.tertiaryContainer) {
return false;
}
if (onTertiaryContainer != scheme.onTertiaryContainer) {
return false;
}
if (error != scheme.error) {
return false;
}
if (onError != scheme.onError) {
return false;
}
if (errorContainer != scheme.errorContainer) {
return false;
}
if (onErrorContainer != scheme.onErrorContainer) {
return false;
}
if (background != scheme.background) {
return false;
}
if (onBackground != scheme.onBackground) {
return false;
}
if (surface != scheme.surface) {
return false;
}
if (onSurface != scheme.onSurface) {
return false;
}
if (surfaceVariant != scheme.surfaceVariant) {
return false;
}
if (onSurfaceVariant != scheme.onSurfaceVariant) {
return false;
}
if (outline != scheme.outline) {
return false;
}
if (shadow != scheme.shadow) {
return false;
}
if (inverseSurface != scheme.inverseSurface) {
return false;
}
if (inverseOnSurface != scheme.inverseOnSurface) {
return false;
}
if (inversePrimary != scheme.inversePrimary) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + primary;
result = 31 * result + onPrimary;
result = 31 * result + primaryContainer;
result = 31 * result + onPrimaryContainer;
result = 31 * result + secondary;
result = 31 * result + onSecondary;
result = 31 * result + secondaryContainer;
result = 31 * result + onSecondaryContainer;
result = 31 * result + tertiary;
result = 31 * result + onTertiary;
result = 31 * result + tertiaryContainer;
result = 31 * result + onTertiaryContainer;
result = 31 * result + error;
result = 31 * result + onError;
result = 31 * result + errorContainer;
result = 31 * result + onErrorContainer;
result = 31 * result + background;
result = 31 * result + onBackground;
result = 31 * result + surface;
result = 31 * result + onSurface;
result = 31 * result + surfaceVariant;
result = 31 * result + onSurfaceVariant;
result = 31 * result + outline;
result = 31 * result + shadow;
result = 31 * result + inverseSurface;
result = 31 * result + inverseOnSurface;
result = 31 * result + inversePrimary;
return result;
}
}

View File

@ -0,0 +1,265 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file is automatically generated. Do not modify it.
package utils;
/**
* Color science utilities.
*
* <p>Utility methods for color science constants and color space conversions that aren't HCT or
* CAM16.
*/
public class ColorUtils {
private ColorUtils() {}
static final double[][] SRGB_TO_XYZ =
new double[][] {
new double[] {0.41233895, 0.35762064, 0.18051042},
new double[] {0.2126, 0.7152, 0.0722},
new double[] {0.01932141, 0.11916382, 0.95034478},
};
static final double[][] XYZ_TO_SRGB =
new double[][] {
new double[] {
3.2413774792388685, -1.5376652402851851, -0.49885366846268053,
},
new double[] {
-0.9691452513005321, 1.8758853451067872, 0.04156585616912061,
},
new double[] {
0.05562093689691305, -0.20395524564742123, 1.0571799111220335,
},
};
static final double[] WHITE_POINT_D65 = new double[] {95.047, 100.0, 108.883};
/** Converts a color from RGB components to ARGB format. */
public static int argbFromRgb(int red, int green, int blue) {
return (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255);
}
/** Returns the alpha component of a color in ARGB format. */
public static int alphaFromArgb(int argb) {
return (argb >> 24) & 255;
}
/** Returns the red component of a color in ARGB format. */
public static int redFromArgb(int argb) {
return (argb >> 16) & 255;
}
/** Returns the green component of a color in ARGB format. */
public static int greenFromArgb(int argb) {
return (argb >> 8) & 255;
}
/** Returns the blue component of a color in ARGB format. */
public static int blueFromArgb(int argb) {
return argb & 255;
}
/** Returns whether a color in ARGB format is opaque. */
public static boolean isOpaque(int argb) {
return alphaFromArgb(argb) >= 255;
}
/** Converts a color from ARGB to XYZ. */
public static int argbFromXyz(double x, double y, double z) {
double[][] matrix = XYZ_TO_SRGB;
double linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z;
double linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z;
double linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z;
int r = delinearized(linearR);
int g = delinearized(linearG);
int b = delinearized(linearB);
return argbFromRgb(r, g, b);
}
/** Converts a color from XYZ to ARGB. */
public static double[] xyzFromArgb(int argb) {
double r = linearized(redFromArgb(argb));
double g = linearized(greenFromArgb(argb));
double b = linearized(blueFromArgb(argb));
return MathUtils.matrixMultiply(new double[] {r, g, b}, SRGB_TO_XYZ);
}
/** Converts a color represented in Lab color space into an ARGB integer. */
public static int argbFromLab(double l, double a, double b) {
double[] whitePoint = WHITE_POINT_D65;
double fy = (l + 16.0) / 116.0;
double fx = a / 500.0 + fy;
double fz = fy - b / 200.0;
double xNormalized = labInvf(fx);
double yNormalized = labInvf(fy);
double zNormalized = labInvf(fz);
double x = xNormalized * whitePoint[0];
double y = yNormalized * whitePoint[1];
double z = zNormalized * whitePoint[2];
return argbFromXyz(x, y, z);
}
/**
* Converts a color from ARGB representation to L*a*b* representation.
*
* @param argb the ARGB representation of a color
* @return a Lab object representing the color
*/
public static double[] labFromArgb(int argb) {
double linearR = linearized(redFromArgb(argb));
double linearG = linearized(greenFromArgb(argb));
double linearB = linearized(blueFromArgb(argb));
double[][] matrix = SRGB_TO_XYZ;
double x = matrix[0][0] * linearR + matrix[0][1] * linearG + matrix[0][2] * linearB;
double y = matrix[1][0] * linearR + matrix[1][1] * linearG + matrix[1][2] * linearB;
double z = matrix[2][0] * linearR + matrix[2][1] * linearG + matrix[2][2] * linearB;
double[] whitePoint = WHITE_POINT_D65;
double xNormalized = x / whitePoint[0];
double yNormalized = y / whitePoint[1];
double zNormalized = z / whitePoint[2];
double fx = labF(xNormalized);
double fy = labF(yNormalized);
double fz = labF(zNormalized);
double l = 116.0 * fy - 16;
double a = 500.0 * (fx - fy);
double b = 200.0 * (fy - fz);
return new double[] {l, a, b};
}
/**
* Converts an L* value to an ARGB representation.
*
* @param lstar L* in L*a*b*
* @return ARGB representation of grayscale color with lightness matching L*
*/
public static int argbFromLstar(double lstar) {
double fy = (lstar + 16.0) / 116.0;
double fz = fy;
double fx = fy;
double kappa = 24389.0 / 27.0;
double epsilon = 216.0 / 24389.0;
boolean lExceedsEpsilonKappa = lstar > 8.0;
double y = lExceedsEpsilonKappa ? fy * fy * fy : lstar / kappa;
boolean cubeExceedEpsilon = fy * fy * fy > epsilon;
double x = cubeExceedEpsilon ? fx * fx * fx : lstar / kappa;
double z = cubeExceedEpsilon ? fz * fz * fz : lstar / kappa;
double[] whitePoint = WHITE_POINT_D65;
return argbFromXyz(x * whitePoint[0], y * whitePoint[1], z * whitePoint[2]);
}
/**
* Computes the L* value of a color in ARGB representation.
*
* @param argb ARGB representation of a color
* @return L*, from L*a*b*, coordinate of the color
*/
public static double lstarFromArgb(int argb) {
double y = xyzFromArgb(argb)[1] / 100.0;
double e = 216.0 / 24389.0;
if (y <= e) {
return 24389.0 / 27.0 * y;
} else {
double yIntermediate = Math.pow(y, 1.0 / 3.0);
return 116.0 * yIntermediate - 16.0;
}
}
/**
* Converts an L* value to a Y value.
*
* <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
*
* <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
* logarithmic scale.
*
* @param lstar L* in L*a*b*
* @return Y in XYZ
*/
public static double yFromLstar(double lstar) {
double ke = 8.0;
if (lstar > ke) {
return Math.pow((lstar + 16.0) / 116.0, 3.0) * 100.0;
} else {
return lstar / 24389.0 / 27.0 * 100.0;
}
}
/**
* Linearizes an RGB component.
*
* @param rgbComponent 0 <= rgb_component <= 255, represents R/G/B channel
* @return 0.0 <= output <= 100.0, color channel converted to linear RGB space
*/
public static double linearized(int rgbComponent) {
double normalized = rgbComponent / 255.0;
if (normalized <= 0.040449936) {
return normalized / 12.92 * 100.0;
} else {
return Math.pow((normalized + 0.055) / 1.055, 2.4) * 100.0;
}
}
/**
* Delinearizes an RGB component.
*
* @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
* @return 0 <= output <= 255, color channel converted to regular RGB space
*/
public static int delinearized(double rgbComponent) {
double normalized = rgbComponent / 100.0;
double delinearized = 0.0;
if (normalized <= 0.0031308) {
delinearized = normalized * 12.92;
} else {
delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
}
return MathUtils.clampInt(0, 255, (int) Math.round(delinearized * 255.0));
}
/**
* Returns the standard white point; white on a sunny day.
*
* @return The white point
*/
public static double[] whitePointD65() {
return WHITE_POINT_D65;
}
static double labF(double t) {
double e = 216.0 / 24389.0;
double kappa = 24389.0 / 27.0;
if (t > e) {
return Math.pow(t, 1.0 / 3.0);
} else {
return (kappa * t + 16) / 116;
}
}
static double labInvf(double ft) {
double e = 216.0 / 24389.0;
double kappa = 24389.0 / 27.0;
double ft3 = ft * ft * ft;
if (ft3 > e) {
return ft3;
} else {
return (116 * ft - 16) / kappa;
}
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file is automatically generated. Do not modify it.
package utils;
/** Utility methods for mathematical operations. */
public class MathUtils {
private MathUtils() {}
/**
* The signum function.
*
* @return 1 if num > 0, -1 if num < 0, and 0 if num = 0
*/
public static int signum(double num) {
if (num < 0) {
return -1;
} else if (num == 0) {
return 0;
} else {
return 1;
}
}
/**
* The linear interpolation function.
*
* @return start if amount = 0 and stop if amount = 1
*/
public static double lerp(double start, double stop, double amount) {
return (1.0 - amount) * start + amount * stop;
}
/**
* Clamps an integer between two integers.
*
* @return input when min <= input <= max, and either min or max otherwise.
*/
public static int clampInt(int min, int max, int input) {
if (input < min) {
return min;
} else if (input > max) {
return max;
}
return input;
}
/**
* Clamps an integer between two floating-point numbers.
*
* @return input when min <= input <= max, and either min or max otherwise.
*/
public static double clampDouble(double min, double max, double input) {
if (input < min) {
return min;
} else if (input > max) {
return max;
}
return input;
}
/**
* Sanitizes a degree measure as an integer.
*
* @return a degree measure between 0 (inclusive) and 360 (exclusive).
*/
public static int sanitizeDegreesInt(int degrees) {
degrees = degrees % 360;
if (degrees < 0) {
degrees = degrees + 360;
}
return degrees;
}
/**
* Sanitizes a degree measure as a floating-point number.
*
* @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive).
*/
public static double sanitizeDegreesDouble(double degrees) {
degrees = degrees % 360.0;
if (degrees < 0) {
degrees = degrees + 360.0;
}
return degrees;
}
/** Distance of two points on a circle, represented using degrees. */
public static double differenceDegrees(double a, double b) {
return 180.0 - Math.abs(Math.abs(a - b) - 180.0);
}
/** Multiplies a 1x3 row vector with a 3x3 matrix. */
public static double[] matrixMultiply(double[] row, double[][] matrix) {
double a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2];
double b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2];
double c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
return new double[] {a, b, c};
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package utils;
/** Utility methods for string representations of colors. */
final class StringUtils {
private StringUtils() {}
/**
* Hex string representing color, ex. #ff0000 for red.
*
* @param argb ARGB representation of a color.
*/
public static String hexFromArgb(int argb) {
int red = ColorUtils.redFromArgb(argb);
int blue = ColorUtils.blueFromArgb(argb);
int green = ColorUtils.greenFromArgb(argb);
return String.format("#%02x%02x%02x", red, green, blue);
}
}

View File

@ -15,6 +15,7 @@ message Settings {
enum ColorScheme {
Default = 0;
BlackAndWhite = 1;
Wallpaper = 2;
}
ColorScheme color_scheme = 6;
bool dim_wallpaper = 7;

View File

@ -385,3 +385,4 @@ dependencyResolutionManagement {
include(":notifications")
include(":accounts")
include(":appshortcuts")
include(":material-color-utilities")

View File

@ -104,6 +104,8 @@ dependencies {
implementation(libs.coil.core)
implementation(libs.coil.compose)
implementation(project(":material-color-utilities"))
implementation(project(":base"))
implementation(project(":i18n"))
implementation(project(":compat"))

View File

@ -71,6 +71,7 @@ fun AppearanceSettingsScreen() {
summary = when (colorScheme) {
ColorScheme.Default -> stringResource(R.string.preference_colors_default)
ColorScheme.BlackAndWhite -> stringResource(R.string.preference_colors_bw)
ColorScheme.Wallpaper -> stringResource(R.string.preference_colors_wallpaper)
else -> null
},
onClick = {

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.settings.colorscheme
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
@ -7,6 +8,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.RadioButtonChecked
import androidx.compose.material.icons.rounded.RadioButtonUnchecked
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -17,44 +19,42 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.Settings.AppearanceSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.theme.getColorScheme
import de.mm20.launcher2.ui.theme.colorSchemeAsState
@Composable
fun ColorSchemeSettingsScreen() {
val viewModel: ColorSchemeSettingsScreenVM = viewModel()
val context = LocalContext.current
PreferenceScreen(title = stringResource(R.string.preference_screen_colors)) {
item {
PreferenceCategory {
val theme by viewModel.theme.observeAsState()
val darkTheme =
theme == AppearanceSettings.Theme.Dark || theme == AppearanceSettings.Theme.System && isSystemInDarkTheme()
val colorScheme by viewModel.colorScheme.observeAsState()
val items = listOf(
val items = mutableListOf(
AppearanceSettings.ColorScheme.Default to R.string.preference_colors_default,
AppearanceSettings.ColorScheme.BlackAndWhite to R.string.preference_colors_bw
AppearanceSettings.ColorScheme.BlackAndWhite to R.string.preference_colors_bw,
)
if (isAtLeastApiLevel(Build.VERSION_CODES.O_MR1)) {
items.add(
AppearanceSettings.ColorScheme.Wallpaper to R.string.preference_colors_wallpaper
)
}
for (cs in items) {
val scheme by colorSchemeAsState(cs.first)
Preference(
title = stringResource(cs.second),
icon = if (colorScheme == cs.first) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
onClick = { viewModel.setColorScheme(cs.first) },
controls = {
ColorSchemePreview(
getColorScheme(
LocalContext.current,
cs.first,
darkTheme,
)
)
ColorSchemePreview(scheme)
}
)
}
@ -65,51 +65,54 @@ fun ColorSchemeSettingsScreen() {
@Composable
fun ColorSchemePreview(colorScheme: ColorScheme) {
Box(
modifier = Modifier
.padding(vertical = 12.dp)
.width(72.dp)
.height(36.dp),
contentAlignment = Alignment.Center
) {
Row(
verticalAlignment = Alignment.CenterVertically
MaterialTheme(colorScheme = colorScheme) {
Box(
modifier = Modifier
.padding(vertical = 12.dp)
.width(72.dp)
.height(36.dp),
contentAlignment = Alignment.Center
) {
Surface(
tonalElevation = 1.dp,
color = colorScheme.surface,
modifier = Modifier
.size(36.dp)
) {}
Surface(
tonalElevation = 1.dp,
color = colorScheme.surfaceVariant,
modifier = Modifier
.size(36.dp)
) {}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Surface(
tonalElevation = 1.dp,
color = colorScheme.primary,
modifier = Modifier
.size(16.dp)
) {}
Surface(
tonalElevation = 1.dp,
color = colorScheme.secondary,
modifier = Modifier
.padding(horizontal = 8.dp)
.size(16.dp)
) {}
Surface(
tonalElevation = 1.dp,
color = colorScheme.tertiary,
modifier = Modifier
.size(16.dp)
) {}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Surface(
tonalElevation = 1.dp,
color = MaterialTheme.colorScheme.surface,
modifier = Modifier
.size(36.dp)
) {}
Surface(
tonalElevation = 1.dp,
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier
.size(36.dp)
) {}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Surface(
tonalElevation = 1.dp,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(16.dp)
) {}
Surface(
tonalElevation = 1.dp,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.padding(horizontal = 8.dp)
.size(16.dp)
) {}
Surface(
tonalElevation = 1.dp,
color = MaterialTheme.colorScheme.tertiary,
modifier = Modifier
.size(16.dp)
) {}
}
}
}
}

View File

@ -1,24 +1,18 @@
package de.mm20.launcher2.ui.theme
import android.content.Context
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.AppearanceSettings
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
import de.mm20.launcher2.ui.theme.colorscheme.DarkBlackAndWhiteColorScheme
import de.mm20.launcher2.ui.theme.colorscheme.DarkPre31DefaultColorScheme
import de.mm20.launcher2.ui.theme.colorscheme.LightBlackAndWhiteColorScheme
import de.mm20.launcher2.ui.theme.colorscheme.LightPre31DefaultColorScheme
import de.mm20.launcher2.ui.theme.colorscheme.*
import de.mm20.launcher2.ui.theme.typography.DefaultTypography
import kotlinx.coroutines.flow.map
import org.koin.androidx.compose.inject
@ -34,34 +28,52 @@ fun LauncherTheme(
val colorSchemePreference by remember { dataStore.data.map { it.appearance.colorScheme } }.collectAsState(
AppearanceSettings.ColorScheme.Default
)
val themePreference by remember { dataStore.data.map { it.appearance.theme } }.collectAsState(
Theme.System
)
val darkTheme =
themePreference == Theme.Dark || themePreference == Theme.System && isSystemInDarkTheme()
val colorScheme by colorSchemeAsState(colorSchemePreference)
MaterialTheme(
colorScheme = getColorScheme(LocalContext.current, colorSchemePreference, darkTheme),
colorScheme = colorScheme,
typography = DefaultTypography,
content = content
)
}
fun getColorScheme(context: Context, colorScheme: AppearanceSettings.ColorScheme, darkTheme: Boolean): ColorScheme {
return when (colorScheme) {
AppearanceSettings.ColorScheme.BlackAndWhite -> {
if (darkTheme) DarkBlackAndWhiteColorScheme
else LightBlackAndWhiteColorScheme
}
else -> {
if (darkTheme) {
if (isAtLeastApiLevel(31)) dynamicDarkColorScheme(context)
else DarkPre31DefaultColorScheme
} else {
if (isAtLeastApiLevel(31)) dynamicLightColorScheme(context)
else LightPre31DefaultColorScheme
@Composable
fun colorSchemeAsState(colorScheme: AppearanceSettings.ColorScheme): MutableState<ColorScheme> {
val context = LocalContext.current
val dataStore: LauncherDataStore by inject()
val themePreference by remember { dataStore.data.map { it.appearance.theme } }.collectAsState(
Theme.System
)
val darkTheme =
themePreference == Theme.Dark || themePreference == Theme.System && isSystemInDarkTheme()
val state = remember(colorScheme, darkTheme) {
mutableStateOf(
when (colorScheme) {
AppearanceSettings.ColorScheme.BlackAndWhite -> {
if (darkTheme) DarkBlackAndWhiteColorScheme else LightBlackAndWhiteColorScheme
}
else -> {
if (darkTheme) {
if (isAtLeastApiLevel(31)) dynamicDarkColorScheme(context)
else DarkPre31DefaultColorScheme
} else {
if (isAtLeastApiLevel(31)) dynamicLightColorScheme(context)
else LightPre31DefaultColorScheme
}
}
}
)
}
if (colorScheme == AppearanceSettings.ColorScheme.Wallpaper && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
val wallpaperColors by wallpaperColorsAsState()
LaunchedEffect(wallpaperColors, darkTheme) {
state.value = WallpaperColorScheme(wallpaperColors, darkTheme)
}
}
}
return state
}

View File

@ -4,6 +4,7 @@ import android.app.WallpaperManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color

View File

@ -0,0 +1,69 @@
package de.mm20.launcher2.ui.theme.colorscheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import de.mm20.launcher2.ui.theme.WallpaperColors
import palettes.TonalPalette
fun WallpaperColorScheme(wallpaperColors: WallpaperColors, darkTheme: Boolean): ColorScheme {
val primary = TonalPalette.fromInt(wallpaperColors.primary.toArgb())
val secondary = TonalPalette.fromInt((wallpaperColors.secondary ?: wallpaperColors.primary).toArgb())
val tertiary = TonalPalette.fromInt((wallpaperColors.tertiary ?: wallpaperColors.primary).toArgb())
val neutral1 = TonalPalette.fromInt(Color.Black.toArgb())
val neutral2 = TonalPalette.fromInt(Color.Black.toArgb())
return if(darkTheme) {
darkColorScheme(
primary = Color(primary.tone(80)),
onPrimary = Color(primary.tone(20)),
primaryContainer = Color(primary.tone(30)),
onPrimaryContainer = Color(primary.tone(90)),
secondary = Color(secondary.tone(80)),
onSecondary = Color(secondary.tone(20)),
secondaryContainer = Color(secondary.tone(30)),
onSecondaryContainer = Color(secondary.tone(90)),
tertiary = Color(tertiary.tone(80)),
onTertiary = Color(tertiary.tone(20)),
tertiaryContainer = Color(tertiary.tone(30)),
onTertiaryContainer = Color(tertiary.tone(90)),
background = Color(neutral1.tone(10)),
onBackground = Color(neutral1.tone(90)),
surface = Color(neutral1.tone(10)),
onSurface = Color(neutral1.tone(90)),
surfaceVariant = Color(neutral2.tone(30)),
onSurfaceVariant = Color(neutral2.tone(80)),
outline = Color(neutral2.tone(60)),
inverseSurface = Color(neutral1.tone(90)),
inverseOnSurface = Color(neutral1.tone(20)),
inversePrimary = Color(primary.tone(40)),
)
} else {
lightColorScheme(
primary = Color(primary.tone(40)),
onPrimary = Color(primary.tone(100)),
primaryContainer = Color(primary.tone(90)),
onPrimaryContainer = Color(primary.tone(10)),
secondary = Color(secondary.tone(40)),
onSecondary = Color(secondary.tone(100)),
secondaryContainer = Color(secondary.tone(90)),
onSecondaryContainer = Color(secondary.tone(10)),
tertiary = Color(tertiary.tone(40)),
onTertiary = Color(tertiary.tone(100)),
tertiaryContainer = Color(tertiary.tone(90)),
onTertiaryContainer = Color(tertiary.tone(10)),
background = Color(neutral1.tone(99)),
onBackground = Color(neutral1.tone(10)),
surface = Color(neutral1.tone(99)),
onSurface = Color(neutral1.tone(10)),
surfaceVariant = Color(neutral2.tone(90)),
onSurfaceVariant = Color(neutral2.tone(30)),
outline = Color(neutral2.tone(50)),
inverseSurface = Color(neutral1.tone(20)),
inverseOnSurface = Color(neutral1.tone(95)),
inversePrimary = Color(primary.tone(80)),)
}
}